How Do You Handle File Uploads on the Web?
Learn how web file uploads work — multipart/form-data, client vs server validation, and pre-signed direct-to-storage uploads.
Expected Interview Answer
Web file uploads are handled by sending file data as multipart/form-data (or via a pre-signed direct-to-storage URL for large files), validating type and size on both client and server, and streaming the bytes to storage rather than buffering the whole file in server memory.
A file input (<input type="file">) gives access to a File object via the File API, which the client can inspect for name, size, and MIME type before upload for fast feedback, then send inside a FormData payload as multipart/form-data (the encoding designed for mixed binary and text fields), or read as bytes for a raw PUT to a pre-signed URL. Client-side checks (extension, size, MIME type) are UX only; the server must re-validate the actual file content — checking real file signatures/magic bytes, not just the extension, since a malicious file can be renamed — and enforce size limits before or during upload to avoid resource exhaustion. For large files or high-scale apps, the common pattern is to have the server issue a short-lived, pre-signed upload URL (e.g. S3 presigned PUT) so the browser uploads directly to object storage, bypassing the app server entirely and avoiding memory pressure from proxying large binary streams through it. Progress can be tracked via the XMLHttpRequest progress event or the Fetch API’s ReadableStream, giving users a live progress bar during upload.
- multipart/form-data correctly carries mixed binary and text fields in one request
- Pre-signed direct-to-storage uploads avoid proxying large files through the app server
- Server-side content validation (magic bytes) prevents disguised malicious files
- Streaming rather than buffering keeps server memory usage bounded regardless of file size
AI Mentor Explanation
File upload handling is like a stadium accepting equipment deliveries: a quick gate check confirms the labeled contents and box size look right, but security still independently inspects the actual contents against the true specification before it enters the ground, because a label can be faked. For very large deliveries, the stadium hands the courier a direct pass to the warehouse loading dock instead of routing every crate through the main office. That gate-check-plus-content-inspection-plus-direct-dock pattern mirrors client validation, server validation, and pre-signed direct uploads.
Step-by-Step Explanation
Step 1
Client-side pre-check
Inspect the File object (name, size, MIME type) for instant UX feedback before upload begins.
Step 2
Send via multipart/form-data or request a pre-signed URL
Small files go through a normal multipart POST; large files get a short-lived pre-signed URL for direct storage upload.
Step 3
Server-side content validation
The server verifies real file signatures/magic bytes and enforces size limits, never trusting the client-reported type alone.
Step 4
Store and reference
The file is streamed to storage (not fully buffered in memory) and its storage key/URL is saved to the database record.
What Interviewer Expects
- Understanding of multipart/form-data and when it is used
- Awareness that client-side file checks are UX only, not security
- Knowledge of pre-signed direct-to-storage uploads for large files
- Mention of streaming vs buffering to control server memory usage
Common Mistakes
- Trusting the file extension or client-reported MIME type as a security check
- Buffering entire large files in server memory instead of streaming them
- Routing every large file through the app server instead of using pre-signed URLs
- Forgetting to enforce a maximum file size limit server-side
Best Answer (HR Friendly)
“For uploads, I check the file type and size in the browser first so users get instant feedback, but I never rely on that alone. The server always re-checks the actual file content, and for large files I generate a pre-signed URL so the browser uploads straight to storage instead of routing everything through my own server.”
Code Example
async function uploadFile(file, onProgress) {
const MAX_BYTES = 10 * 1024 * 1024
if (!['image/png', 'image/jpeg'].includes(file.type) || file.size > MAX_BYTES) {
throw new Error('Only PNG/JPEG under 10MB are allowed')
}
const formData = new FormData()
formData.append('avatar', file)
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('POST', '/api/uploads')
xhr.upload.onprogress = e => onProgress(e.loaded / e.total)
xhr.onload = () => (xhr.status === 200 ? resolve(xhr.response) : reject(xhr.statusText))
xhr.onerror = () => reject(new Error('Upload failed'))
xhr.send(formData)
})
}Follow-up Questions
- Why should the server check magic bytes instead of trusting the file extension?
- How does a pre-signed upload URL work and why does it reduce server load?
- How would you resume an interrupted large file upload?
- What server-side risks does an unvalidated file upload endpoint introduce?
MCQ Practice
1. Why is multipart/form-data used for file uploads instead of application/json?
multipart/form-data is specifically designed to combine binary file data with regular text fields.
2. Why should a server check file magic bytes rather than trusting the client-reported MIME type?
Client-reported type and extension are attacker-controlled; verifying real file signatures is the trustworthy check.
3. What is the main benefit of a pre-signed direct-to-storage upload URL?
Pre-signed URLs let the client upload directly to storage, avoiding memory pressure on the app server.
Flash Cards
What encoding carries file uploads? — multipart/form-data, for mixed binary and text fields.
Is client-side file type checking secure? — No — always re-verify actual content server-side via magic bytes.
How are large files uploaded efficiently? — Via a pre-signed URL direct to storage, bypassing the app server.
How should the server handle file bytes? — Stream them rather than fully buffering in memory.