How to Upload Files in React and Node.js

September 27, 2026 · 3 views

Uploading a file sounds like a five-minute job until you actually wire up file upload in React and Node.js and hit a wall: the request either arrives with an empty body, Multer throws MulterError: Unexpected field, or the browser silently strips your Content-Type header. None of that is random bad luck — it's what happens when you treat a file upload like a normal JSON request. This guide walks through the real, working setup: a React form that sends FormData, an Express API that parses it with Multer, and the specific mistakes that break this exact flow in production.

Why file uploads aren't just another API call

A typical POST request sends JSON: you JSON.stringify an object, set Content-Type: application/json, and Express parses it with express.json(). A file breaks that model completely. Binary data doesn't serialize into a string cleanly, and a form usually needs to send text fields (a caption, a user ID) alongside the binary payload in a single request.

The browser and the server solve this with multipart/form-data: the request body is split into named parts, each with its own headers, separated by a boundary string the browser generates automatically. Express's built-in express.json() and express.urlencoded() middleware don't understand this format at all — they'll just skip the body — which is why req.body shows up empty and confuses people the first time they see it. You need a middleware built specifically for multipart parsing, which for a Node.js API is almost always Multer.

Setting up the Node.js API with Multer

Install Multer and wire it into the specific route that accepts a file, not globally on the whole app:

const express = require('express');
const multer = require('multer');
const path = require('path');

const app = express();

const storage = multer.diskStorage({
  destination: (req, file, cb) => cb(null, 'uploads/'),
  filename: (req, file, cb) => {
    const unique = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
    cb(null, `${unique}${path.extname(file.originalname)}`);
  },
});

const upload = multer({
  storage,
  limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
  fileFilter: (req, file, cb) => {
    const allowed = ['image/png', 'image/jpeg', 'image/webp'];
    if (!allowed.includes(file.mimetype)) {
      return cb(new Error('Only PNG, JPEG, or WEBP files are allowed'));
    }
    cb(null, true);
  },
});

app.post('/api/upload', upload.single('file'), (req, res) => {
  if (!req.file) {
    return res.status(400).json({ error: 'No file received' });
  }
  res.status(201).json({
    filename: req.file.filename,
    size: req.file.size,
    url: `/uploads/${req.file.filename}`,
  });
});

upload.single('file') is the piece people get wrong most often: the string 'file' must match the field name the client sends its file under, exactly. A mismatch here is the single most common cause of req.file being undefined with no error thrown at all.

Sending the file from React

On the frontend, the file comes from an <input type="file"> element, and it has to be appended to a FormData object — never sent as JSON:

function UploadForm() {
  const [status, setStatus] = useState('idle');

  const handleSubmit = async (e) => {
    e.preventDefault();
    const fileInput = e.target.elements.file;
    const file = fileInput.files[0];
    if (!file) return;

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

    setStatus('uploading');
    try {
      const res = await fetch('/api/upload', {
        method: 'POST',
        body: formData,
      });
      if (!res.ok) throw new Error('Upload failed');
      setStatus('done');
    } catch (err) {
      setStatus('error');
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <input type="file" name="file" accept="image/png,image/jpeg,image/webp" />
      <button type="submit" disabled={status === 'uploading'}>Upload</button>
    </form>
  );
}

The field name passed to formData.append('file', file) is exactly what must match the string inside upload.single('file') on the backend.

Common mistakes that break this flow

  • Manually setting Content-Type: multipart/form-data on the fetch call. The browser needs to generate its own boundary string and append it to the header itself — overriding it manually produces a body Multer can't parse, and you'll see an empty req.body with no clear error.
  • Using multer.memoryStorage() for large files or high-traffic uploads without limits. Memory storage buffers the whole file in RAM before your handler ever runs, which is fine for small avatars but will crash a Node process under real load. Disk storage, or streaming straight to S3/Cloudinary, scales far better.
  • Forgetting limits.fileSize. Without it, a user can upload a multi-gigabyte file and take down the server's disk or memory before your validation code ever runs.
  • Skipping fileFilter and trusting the file extension. A .png extension doesn't guarantee PNG content — validate file.mimetype, and for anything security-sensitive, verify the actual file signature server-side too.

Best practices for production uploads

  1. Validate file type and size on both the client (for a fast UI response) and the server (because client-side checks are trivially bypassed).
  2. Generate a new filename server-side rather than trusting file.originalname — it can contain path traversal characters or collide with existing files.
  3. For anything beyond small images, upload directly to object storage (S3, Cloudflare R2, Backblaze) using a presigned URL, so large files never pass through your Node.js process at all.
  4. Return a clear, structured error response (not just a raw 500) when fileFilter or limits rejects a file, so the frontend can show the user what actually went wrong.

Frequently Asked Questions

Why is req.body empty when I upload a file? Because express.json() and express.urlencoded() don't parse multipart/form-data bodies at all. You need Multer (or an equivalent multipart parser) on that specific route.

Can I upload multiple files in one request? Yes — use upload.array('files', maxCount) instead of upload.single(), and append each file to the same FormData key on the client with formData.append('files', file) in a loop.

Do I need Multer if I'm uploading straight to S3 from the browser? No — if you're using presigned URLs, the browser uploads the file directly to S3 and your Node.js API only issues the signed URL. Multer is only needed when the file passes through your own server.

Why does my upload work locally but fail behind a reverse proxy? Check the proxy's own body size limit (Nginx's client_max_body_size, for example) — it can reject large uploads before the request ever reaches Multer's limits.fileSize check, which produces a confusing 413 error unrelated to your Express config.

Key takeaways

File uploads fail in React and Node.js almost always for one of two reasons: the request wasn't sent as multipart/form-data, or the field name on the client doesn't match the field name Multer expects on the server. Get those two things right, add real size and type validation on both ends, and move anything beyond small files to direct-to-storage uploads with presigned URLs before your Node.js process becomes the bottleneck.

#nodejs #express #react #file-upload #multer #formdata
Share this article:

0 Comments

No comments yet — be the first to share your thoughts.

Leave a comment

Never published.