How File Uploads Work in a MERN Application

Every MERN Blog needs file uploads โ€” profile avatars, post cover images, perhaps inline images in post bodies. Uploading a file is fundamentally different from submitting JSON: the browser must encode the binary file data alongside any text fields using the multipart/form-data content type, and the Express server needs dedicated middleware to parse and store that binary data before the route handler runs. Understanding this flow end-to-end โ€” how the browser packages the file, how Multer receives it, where it is stored, and how React displays the result โ€” is the foundation for every file upload feature in the MERN Blog.

The File Upload Flow in MERN

React (browser):
  1. User selects a file using an <input type="file">
  2. React reads the File object (name, size, type, lastModified)
  3. React creates a FormData object and appends the file + any other fields
  4. Axios sends a POST request with Content-Type: multipart/form-data
        โ”‚
        โ–ผ
Express (server):
  5. Request arrives with multipart body
  6. Multer middleware parses the multipart body
  7. Multer stores the file (disk or cloud storage)
  8. Multer attaches file metadata to req.file (or req.files for multiple)
  9. Route handler reads req.file โ€” saves the URL to MongoDB
  10. Handler returns the file URL in the JSON response
        โ”‚
        โ–ผ
React (browser):
  11. Axios receives the response with the file URL
  12. React updates state โ€” displays the image immediately
Note: multipart/form-data is the only encoding that supports binary file data in an HTTP request. Unlike application/json (which only handles text), multipart encoding splits the request body into named parts, each with its own content type. This is why you cannot send a file in a regular JSON POST โ€” you need a FormData object on the client and Multer on the server. Never manually set the Content-Type header for file uploads in Axios โ€” let it be set automatically (with the correct boundary string) by passing a FormData object as the request body.
Tip: For development, Multer’s disk storage (saving files to the local filesystem) is the fastest way to get file uploads working. For production, you should store files in cloud object storage โ€” Cloudinary, AWS S3, or Google Cloud Storage. These services provide CDN delivery, automatic image resizing, and reliable storage that persists even if your Express server restarts or scales horizontally. Lesson 4 covers the Cloudinary integration.
Warning: Never trust client-supplied file metadata. The browser reports the file’s MIME type (e.g. image/jpeg) but this can be spoofed โ€” a user can rename a PHP script to photo.jpg and the browser will report image/jpeg. On the server, use Multer’s fileFilter callback to check the MIME type, and consider also checking the file’s magic bytes (first few bytes of the binary content) for stricter validation. Never execute uploaded files and always serve them from a separate domain or CDN.

The multipart/form-data Request Structure

HTTP POST /api/upload
Content-Type: multipart/form-data; boundary=----FormBoundaryABC123

------FormBoundaryABC123
Content-Disposition: form-data; name="avatar"; filename="profile.jpg"
Content-Type: image/jpeg

[binary JPEG data here โ€” thousands of bytes]
------FormBoundaryABC123
Content-Disposition: form-data; name="userId"

64a1f2b3c8e4d5f6a7b8c9d0
------FormBoundaryABC123--

Each "part" has its own headers and content.
Multer parses these parts and gives Express access to:
  req.file  โ†’ { fieldname, originalname, mimetype, size, path, filename }
  req.body  โ†’ { userId: '64a1f...' }  (other text fields)

What Multer Gives You

Property Example Use For
req.file.fieldname ‘avatar’ Which input field sent the file
req.file.originalname ‘profile.jpg’ The original filename from the browser
req.file.mimetype ‘image/jpeg’ MIME type (validate server-side)
req.file.size 245760 File size in bytes
req.file.filename ‘avatar-1710000000-123.jpg’ Saved filename on disk
req.file.path ‘uploads/avatar-1710000000-123.jpg’ Full path to the stored file
req.file.buffer Buffer File content in memory (memoryStorage only)

Common Mistakes

Mistake 1 โ€” Setting Content-Type manually for file uploads

โŒ Wrong โ€” manually setting Content-Type breaks the boundary string:

await axios.post('/api/upload', formData, {
  headers: { 'Content-Type': 'multipart/form-data' }, // breaks boundary!
});

โœ… Correct โ€” omit Content-Type and let Axios set it automatically:

await axios.post('/api/upload', formData); // โœ“ Axios sets the correct boundary

Mistake 2 โ€” Sending a file as JSON

โŒ Wrong โ€” trying to base64-encode the file and send it in a JSON body:

const base64 = await readFileAsBase64(file);
await axios.post('/api/upload', { avatar: base64 }); // 33% size overhead, not standard

โœ… Correct โ€” use FormData for binary file uploads:

const fd = new FormData();
fd.append('avatar', file);
await axios.post('/api/upload', fd); // โœ“ multipart/form-data

Mistake 3 โ€” Serving uploaded files from the wrong URL

โŒ Wrong โ€” returning a server-local path to React:

res.json({ url: req.file.path }); // 'uploads/avatar-123.jpg' โ€” not accessible from browser!

โœ… Correct โ€” construct a full public URL:

const url = `${process.env.SERVER_URL}/uploads/${req.file.filename}`;
res.json({ url }); // 'https://api.mernblog.com/uploads/avatar-123.jpg' โœ“

Quick Reference

Concept Key Point
Encoding type multipart/form-data โ€” required for file uploads
Client-side object FormData โ€” append file with fd.append(‘field’, file)
Server-side middleware Multer โ€” parses multipart body, stores file
Single file req.file โ€” after upload.single(‘fieldname’) middleware
Multiple files req.files โ€” after upload.array(‘fieldname’, maxCount)
Storage options DiskStorage (dev), MemoryStorage, CloudinaryStorage (prod)

🧠 Test Yourself

You set headers: { 'Content-Type': 'multipart/form-data' } manually on your Axios file upload request. The server receives the request but Multer cannot parse the file. Why?