How to Upload Files in JavaScript with Fetch API

April 14, 2026 · 8 min read

Uploading a file in JavaScript is straightforward once you know the two parts: FormData to package the file and fetch to send it. This guide covers the complete pattern for browsers and Node.js, with working examples, progress tracking, and the one mistake that causes 400 errors on every second try.

Basic File Upload with Fetch and FormData

The pattern is the same whether you have a file from an <input type="file"> or a File object from a drag-and-drop handler:

const file = document.querySelector('input[type="file"]').files[0];

const form = new FormData();
form.append('file', file);

const response = await fetch('https://httpbin.org/post', {
  method: 'POST',
  body: form
});

const data = await response.json();
console.log(data);

FormData encodes the body as multipart/form-data, which is the correct format for binary file uploads. Do not set a Content-Type header. When you pass a FormData object as the body, fetch sets Content-Type automatically and appends the boundary string that separates parts in the request body. If you override it, the boundary disappears and the server returns a 400.

Adding API Key Authentication

Pass headers for authentication separately from the body. The key is to include authentication headers but leave out Content-Type:

const API_KEY = 'your_api_key_here';

async function uploadFile(file) {
  const form = new FormData();
  form.append('file', file);

  const response = await fetch('https://api.example.com/upload', {
    method: 'POST',
    headers: {
      'X-API-Key': API_KEY
      // Do NOT add Content-Type here
    },
    body: form
  });

  if (!response.ok) {
    throw new Error(`Upload failed: ${response.status} ${response.statusText}`);
  }

  return response.json();
}

Uploading to the FilePost API

FilePost takes one request and returns a permanent CDN URL. No AWS account, no storage configuration:

const API_KEY = 'your_api_key_here';

async function uploadToFilePost(file) {
  const form = new FormData();
  form.append('file', file);

  const response = await fetch('https://filepost.dev/v1/upload', {
    method: 'POST',
    headers: { 'X-API-Key': API_KEY },
    body: form
  });

  if (!response.ok) {
    const err = await response.json();
    throw new Error(err.detail || 'Upload failed');
  }

  const { url, file_id, size } = await response.json();
  console.log('Uploaded:', url);
  // https://cdn.filepost.dev/file/filepost/uploads/a1/a1b2c3.png
  return url;
}

File Input with Upload Button (Complete Example)

A minimal but complete browser example with error handling and status feedback:

<input type="file" id="fileInput">
<button onclick="handleUpload()">Upload</button>
<p id="status"></p>

<script>
const API_KEY = 'your_api_key_here';

async function handleUpload() {
  const file = document.getElementById('fileInput').files[0];
  const status = document.getElementById('status');

  if (!file) {
    status.textContent = 'Please select a file first.';
    return;
  }

  status.textContent = 'Uploading...';

  try {
    const form = new FormData();
    form.append('file', file);

    const response = await fetch('https://filepost.dev/v1/upload', {
      method: 'POST',
      headers: { 'X-API-Key': API_KEY },
      body: form
    });

    if (!response.ok) throw new Error(`HTTP ${response.status}`);

    const { url } = await response.json();
    status.innerHTML = `Done: <a href="${url}" target="_blank">${url}</a>`;
  } catch (err) {
    status.textContent = `Upload failed: ${err.message}`;
  }
}
</script>

Drag and Drop Upload

The File objects from a drop event work exactly the same way as those from a file input:

const dropzone = document.getElementById('dropzone');

dropzone.addEventListener('dragover', (e) => {
  e.preventDefault();
  dropzone.classList.add('drag-over');
});

dropzone.addEventListener('drop', async (e) => {
  e.preventDefault();
  dropzone.classList.remove('drag-over');

  const file = e.dataTransfer.files[0];
  if (!file) return;

  const form = new FormData();
  form.append('file', file);

  const response = await fetch('https://filepost.dev/v1/upload', {
    method: 'POST',
    headers: { 'X-API-Key': API_KEY },
    body: form
  });

  const { url } = await response.json();
  console.log('Uploaded:', url);
});

Upload Progress with XMLHttpRequest

The fetch API does not expose upload progress events. If you need a progress bar, use XMLHttpRequest, which fires xhr.upload.onprogress as the file transfers:

function uploadWithProgress(file, apiKey, onProgress) {
  return new Promise((resolve, reject) => {
    const form = new FormData();
    form.append('file', file);

    const xhr = new XMLHttpRequest();

    xhr.upload.onprogress = (event) => {
      if (event.lengthComputable) {
        const pct = Math.round((event.loaded / event.total) * 100);
        onProgress(pct);
      }
    };

    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(JSON.parse(xhr.responseText));
      } else {
        reject(new Error(`Upload failed: ${xhr.status}`));
      }
    };

    xhr.onerror = () => reject(new Error('Network error'));

    xhr.open('POST', 'https://filepost.dev/v1/upload');
    xhr.setRequestHeader('X-API-Key', apiKey);
    xhr.send(form);
  });
}

// Usage:
uploadWithProgress(file, API_KEY, (pct) => {
  progressBar.style.width = pct + '%';
  label.textContent = pct + '%';
}).then(({ url }) => {
  console.log('Done:', url);
});

Node.js File Upload (v18+)

Node.js 18 and later include native fetch and FormData. Use fs.readFileSync or a Blob from a Buffer:

import fs from 'fs';

const API_KEY = 'your_api_key_here';
const fileBuffer = fs.readFileSync('./report.pdf');
const blob = new Blob([fileBuffer], { type: 'application/pdf' });

const form = new FormData();
form.append('file', blob, 'report.pdf');

const response = await fetch('https://filepost.dev/v1/upload', {
  method: 'POST',
  headers: { 'X-API-Key': API_KEY },
  body: form
});

const { url } = await response.json();
console.log(url);

For Node.js versions before 18, install the node-fetch and form-data packages. The API is identical, just swap the imports and everything else stays the same.

Frequently Asked Questions

How do I upload a file in JavaScript with Fetch?

Create a FormData object, append your file, and pass it as the body of a fetch POST request. Do not set Content-Type. Fetch sets it automatically with the correct multipart boundary.

Why do I get a 400 error when uploading with fetch?

Almost always because Content-Type was set manually in headers. When you pass FormData as the body, fetch generates the multipart boundary and embeds it in Content-Type. If you override it, the boundary is stripped and the server cannot parse the request. Remove Content-Type from your headers entirely.

How do I track upload progress in JavaScript?

Use XMLHttpRequest instead of fetch. XHR exposes xhr.upload.onprogress, which fires with event.loaded and event.total as the file transfers. Calculate percentage as Math.round((event.loaded / event.total) * 100).

Does this work in Node.js?

Yes. Node.js 18+ has native fetch and FormData. Create a Blob from a Buffer and append it to FormData. For older versions, the node-fetch and form-data npm packages provide an identical API.

One API Call, One URL Back

FilePost handles the storage, CDN, and delivery. You get 30 free uploads per month, no credit card, no setup.

Get Your Free API Key