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
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.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) |