Skip to main content
Fetch API - Uploading Files

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.