Presigned Uploads: A 5-Step Guide to Moving Large AI Images
Short answer: for a marketplace that stores AI-generated images, use a presigned upload. Your Node.js service authenticates the caller, creates a short-lived upload URL, and lets the client send the bytes directly to object storage. Access keys stay on the server. The application server does not be

Short answer: for a marketplace that stores AI-generated images, use a presigned upload. Your Node.js service authenticates the caller, creates a short-lived upload URL, and lets the client send the bytes directly to object storage. Access keys stay on the server. The application server does not become a very expensive pipe for a 200 MB image.
The useful contract is small: create a URL for one bucket and key, preserve the headers used to sign it, then issue an HTTP PUT with the image bytes. The storage provider can change behind that contract while the upload code stays stable.
That is a fair shortlist, not a winner-takes-all ranking. S3 has the broadest default assumption in many teams. R2 and OSS make sense when their cloud context is already a strong fit. Infrai is interesting when swapping the service behind one contract matters: the calling code can keep the presigned-upload shape while the provider behind the capability moves.
For large-file throughput, measure the path from the uploader to the object store. A presigned URL removes your Node.js process from the byte path, but it does not make a slow client fast. For generated images, use a stable object key per asset, set the exact content type, and decide what should happen if that key is reused before you ship.
Picture the request path. A worker creates a 200 MB image. Your API authenticates the marketplace request and returns an upload URL. The worker sends those bytes directly to storage, while the API remains free to handle metadata, authorization, and the next job. If the worker retries, the same key and the same signed headers keep the decision visible: overwrite this asset, or reject it in the application layer. That choice belongs in your data model, because this upload contract does not provide If-Match conditional writes or object versioning.
The presign request targets POST /v1/storage/object/presign/{bucket}/{key}. The generated request must include Content-Type and any other required headers. Those headers are part of the signed request; changing them during the upload can invalidate the signature.
The following keeps both halves in one small flow. It asks Infrai for a presigned URL, then uploads a local image with Node.js's built-in fetch. The request uses the documented storage route, reads the key from an environment variable, and checks every response instead of treating every response as success.
import { readFile } from " node:fs/promises "; const apiKey = process. env. INFRAI_API_KEY; const apiBaseUrl = process. env. INFRAI_API_BASE_URL; if (! apiKey ||! apiBaseUrl ) { throw new Error ( " INFRAI_API_KEY and INFRAI_API_BASE_URL are required " ); } async function requestPresignedUrl ( bucket: string, key: string, contentType: string, ): Promise < string > { const path = [ " v1 ", " storage ", " object ", " presign ", encodeURIComponent ( bucket ), encodeURIComponent ( key ), ]. join ( " / " ); const endpoint = apiBaseUrl. replace ( / \/ $/, "" ) + " / " + path; let response: Response; for ( let attempt = 0; attempt < 3; attempt += 1 ) { response = await fetch ( endpoint, { method: " POST ", headers: { Authorization: `Bearer ${ apiKey } `, " Content-Type ": " application/json ", " Idempotency-Key ": key, }, body: JSON. stringify ({ content_type: contentType }), }); if ( response. status!== 429 ) break; const retryAfter = Number ( response. headers. get ( " Retry-After " )??
" 1 " ); await new Promise (( resolve ) => setTimeout ( resolve, retryAfter * 1000 )); } if (! response!. ok ) { throw new Error ( `Presign failed ( ${ response!. status } ): ${ await response!. text ()} ` ); } const result = ( await response!. json ()) as { url: string }; if (! result. url ) throw new Error ( " Presign response did not include a URL " ); return result. url; } export async function uploadImage ( bucket: string, key: string, imagePath: string, contentType = " image/png ", ): Promise < void > { const presignedUrl = await requestPresignedUrl ( bucket, key, contentType ); const image = await readFile ( imagePath ); const response = await fetch ( presignedUrl, { method: " PUT ", headers: { " Content-Type ": contentType, }, body: image, }); if (! response. ok ) { const detail = await response. text (); throw new Error ( `Image upload failed ( ${ response. status } ): ${ detail } ` ); } } Enter fullscreen mode Exit fullscreen mode This code deliberately does not put an access key in the client. Your trusted backend asks for the presigned URL, then returns only the URL and the exact upload headers to the caller. Keep that URL short-lived in the surrounding design, and never treat it as a permanent download link.
The request body and response parser must match the storage capability's current schema; this example keeps the signed content type explicit because it must match the later PUT.
For a large file, replace the in-memory readFile step with a stream supported by the runtime and your storage client. The principle stays the same: PUT, exact signed headers, and a checked status. If the upload needs a retry, make the object key and retry policy explicit. A retry that writes to a different key can leave an orphaned image; a retry that overwrites a key can lose the earlier object.
Choose S3 when existing AWS authorization, tooling, and operational knowledge outweigh the cost of another service boundary. Choose R2 when its S3-compatible model and Cloudflare placement match the rest of the product. Choose OSS when Alibaba Cloud is already the place where your data, identity, and deployment live.
Choose the one-contract approach when the important requirement is portability across backend capabilities. Infrai's storage surface exposes the presign route, while the broader platform uses a single REST API and key. That can keep application code focused on a capability contract instead of a provider SDK. It does not erase the need to test throughput, regional behavior, and recovery for the image sizes your marketplace actually accepts.
Infrai is the wrong choice for a public image host, immutable financial records, or automatic cross-cloud migration. Pick S3, R2, or OSS when its ecosystem supplies the missing operational feature. The trade-off is straightforward: fewer platform-specific interfaces versus fewer storage guarantees.
The comparison has a practical consequence: do not choose from a feature checklist alone. Run a representative upload test with the same image sizes, concurrency, client network, and key layout. Record completion time and failed uploads. Throughput is a path property.
This pattern protects credentials during upload. It does not turn stored objects into public assets. There is no public or public-read ACL, so public_url remains null; permanent public links and a static image-hosting workflow are outside this fit.
There is no object versioning or object lock. An accidental overwrite is not made recoverable by the presign step, and financial-grade WORM behavior needs an external design. There is also no If-Match conditional write, so strict mutual exclusion belongs in a queue or database.
Browser-direct uploads have another boundary: there is no independent set_cors route for self-service CORS configuration. A browser workflow must account for that before it is selected. Lifecycle expiration is at least one day, multipart fragments do not have automatic cleanup rules, and metadata cannot be searched server-side beyond prefix filtering. Cross-region automatic replication and bulk cross-cloud migration are not provided; supported vendor coverage includes R2, S3, OSS, and COS, not GCS or B2.
These limits are decision inputs, not footnotes. If the requirement is private, expiring access to large marketplace uploads, presigned URLs are a clean fit. If the requirement is a public CDN origin, immutable retention, or automated multi-cloud movement, start with a storage system designed for that requirement.