Fetch API - Uploading Files
File uploads use FormData. The browser builds a multipart body, includes the binary content, and sets the boundary in the Content-Type header automatically.
Single file upload
const file = input.files[0];
const form = new FormData();
form.append('document', file);
const response = await fetch('/api/documents', {
method: 'POST',
body: form
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`);
}
Do not set Content-Type manually. Doing so removes the boundary the server needs to parse the parts.
Validating before sending
const MAX_BYTES = 5 * 1024 * 1024;
const allowed = ['image/jpeg', 'image/png', 'application/pdf'];
function validate(file) {
if (!allowed.includes(file.type)) {
throw new Error('Unsupported file type');
}
if (file.size > MAX_BYTES) {
throw new Error('File is larger than 5 MB');
}
}
Client-side checks improve feedback but never replace server-side validation; a crafted request can bypass the browser entirely.
Multiple files and metadata
const form = new FormData();
form.append('folder', 'invoices');
for (const file of input.files) {
form.append('files', file);
}
await fetch('/api/documents/bulk', { method: 'POST', body: form });
Upload progress
Fetch has limited upload progress support. Where it is unavailable, either show an indeterminate state or use XMLHttpRequest for that specific flow.
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
progress.value = (event.loaded / event.total) * 100;
}
};
Receiving the server response
const result = await response.json();
console.log(result.url, result.size, result.checksum);
Store the server-provided identifier, not the local file name, when linking the upload elsewhere in the application.
Large files
For files measured in gigabytes, use chunked uploads: split the file into parts, send each part with its index, and ask the server to assemble them. This allows resuming after a failure and avoids single-request timeouts.