# File Upload > Presigned URLs, chunked uploads, progress UI, and retry. Frontend architecture guidance from fearchitect, written by Abas Turabli and last reviewed 2026-06-21. Source: https://fearchitect.com/topics/file-upload Use it as reference for the task at hand. Before changing code, check this guidance against the codebase: where the code already makes a different, deliberate choice, flag the conflict instead of rewriting it. Library APIs move faster than this guide, so confirm exact signatures in the official docs linked at the end. ## 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. ## Presigned 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. ```ts async function uploadFile( file: File, presignedUrl: string, onProgress: (pct: number) => void, ): Promise { // 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. ## Diagram ```mermaid sequenceDiagram participant B as Browser participant API as Your API participant S3 as Object Storage B->>API: POST /upload/sign (name, size, type) API-->>B: { presignedUrl, key } B->>S3: PUT presignedUrl (file bytes) S3-->>B: 200 OK B->>API: POST /upload/complete (key) API-->>B: { fileId } ``` Your server signs the URL and records metadata — it never proxies the file bytes. ## Note: 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. ## 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. ## Related topics - [Network Performance](https://fearchitect.com/topics/network-performance.md): Hint, prioritize, and pre-navigate to cut request latency. - [Real-time: WebSockets vs SSE vs Polling](https://fearchitect.com/topics/realtime-websockets-sse-polling.md): Match the right real-time transport to your data-flow direction. - [Caching Strategies](https://fearchitect.com/topics/caching-strategies.md): Layer browser, CDN, and app caches to serve responses without re-fetching. - [Backend for Frontend (BFF)](https://fearchitect.com/topics/backend-for-frontend.md): A per-client server layer that shapes and aggregates APIs for one frontend. - [Autocomplete / Typeahead](https://fearchitect.com/topics/autocomplete-typeahead.md): Debounced input, AbortController cancellation, and ARIA combobox. - [Data Table / Data Grid](https://fearchitect.com/topics/data-table.md): Headless table logic separate from rendering, with server-side ops for large datasets. ## Further reading - [AWS S3 — Uploading objects using presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html) - [AWS S3 — Multipart upload overview](https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html) - [TUS resumable upload protocol](https://tus.io/protocols/resumable-upload) - [MDN — XMLHttpRequest.upload](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/upload) - [MDN — DataTransfer.files](https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/files)