Summary
File uploads go directly from browser to object storage via presigned URLs — your server never proxies the bytes. Large files need chunked, resumable uploads so a network drop does not restart from zero. Client-side validation (type, size) gives immediate feedback; XHR progress events drive the progress bar. Drag-and-drop uses the DataTransfer API.
Jump to the interview anglePresigned URL upload
A presigned URL is a time-limited, pre-authenticated URL issued by your backend that lets the browser PUT a file directly to object storage (S3, GCS, R2). Your server signs the URL with credentials it already holds, returns the URL to the client, and the client streams the file straight to storage. Your origin never sees the payload — bandwidth, CPU, and memory are offloaded entirely to the storage service.
Presigned upload flow
- 1
Client requests a presigned URL
Send a lightweight POST to your API with file name, size, and MIME type. The server validates the request — size caps, allowed types, auth — then calls the storage SDK to generate a signed PUT URL, typically valid for 15 minutes.
- 2
Browser PUTs directly to storage
The client PUTs the file binary to the presigned URL. The storage service validates the signature and writes the object. CORS on the bucket must allow PUT from your origin. Use XHR (not fetch) here if you need upload progress events.
- 3
Client notifies the backend
After the PUT succeeds (HTTP 200/204), the client calls a second API endpoint with the storage key. The backend records the metadata, triggers any post-processing (virus scan, transcoding), and marks the upload complete.
Upload with progress via XHR
fetch lacks upload progress. XHR exposes xhr.upload.onprogress, which fires as bytes leave the browser. Wire it to a state variable; the presigned URL is already in hand before this runs.
async function uploadFile(
file: File,
presignedUrl: string,
onProgress: (pct: number) => void,
): Promise<void> {
// Client-side validation before the network round-trip.
const ALLOWED = ["image/jpeg", "image/png", "application/pdf"];
const MAX_BYTES = 100 * 1024 * 1024; // 100 MB
if (!ALLOWED.includes(file.type)) throw new Error("Unsupported file type");
if (file.size > MAX_BYTES) throw new Error("File exceeds 100 MB limit");
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) onProgress(Math.round((e.loaded / e.total) * 100));
};
xhr.onload = () => {
if (xhr.status === 200 || xhr.status === 204) resolve();
else reject(new Error(`Storage PUT failed: ${xhr.status}`));
};
xhr.onerror = () => reject(new Error("Network error during upload"));
xhr.open("PUT", presignedUrl);
xhr.setRequestHeader("Content-Type", file.type);
xhr.send(file); // streams the File blob; no base64, no FormData
});
}xhr.upload.onprogress fires per chunk as bytes leave the OS buffer. fetch's ReadableStream alternative requires a TransformStream to count bytes and does not work in all environments.
Chunked and resumable uploads for large files
For files over ~100 MB, use S3 Multipart Upload or the TUS protocol. Both split the file into parts (5 MB minimum for S3), upload each part independently, and let a stalled upload resume from the last completed part. The browser tracks which parts are done in localStorage or a server-side session. TUS has an open spec with client libraries for browsers.
Drag-and-drop and concurrency
- Attach dragover (call preventDefault) and drop listeners; read files from event.dataTransfer.files.
- Cap concurrent uploads — 3 at once is a safe default; more saturates the connection and blocks progress feedback.
- Retry transient failures (5xx, network error) with exponential backoff; never retry 4xx responses.
- Revoke object URLs created with URL.createObjectURL after preview to free memory.
Interview angle
Interviewers want the presigned-URL flow, why you do not proxy through your server, how chunked uploads survive network drops, and how XHR exposes upload progress that fetch does not.
Soundbite: "Generate a presigned URL server-side, PUT the file straight to S3, then notify the backend — your server never touches the bytes."
Key terms
- Presigned URL
- Time-limited signed URL that authorises one specific HTTP operation (PUT/GET) on an object storage resource without exposing credentials.
- S3 Multipart Upload
- S3 API that splits a file into parts (min 5 MB each), uploads them in parallel, and assembles the object server-side.
- TUS protocol
- Open resumable upload protocol; tracks byte offset server-side so interrupted uploads resume from where they stopped.
- DataTransfer.files
- FileList exposed by the browser's drop event containing files dragged onto the drop target.
- xhr.upload.onprogress
- XHR event that fires as upload bytes leave the browser, enabling per-file progress tracking.