When we build a backend using Node.js and Express, we usually handle normal data like:
- name
- password
- age
This data is sent from frontend to backend in JSON format.
Example:
{
"name": "Dipika",
"email": "dipika@gmail.com"
}In Express, we handle this using:
app.use(express.json())Now Express can read this data using:
req.body2️⃣ But What If We Want to Upload Files?
Now imagine we want users to upload:
- Profile image
- Resume (PDF)
- Blog thumbnail
- Product image
Can we send files in JSON format?
❌ No.
Because JSON is made for text data, not files.
Files like images and PDFs are binary data, not simple text.
So JSON cannot properly send them.
3️⃣ Then How Does the Browser Send Files?
When we upload a file using a form like this:
<form enctype="multipart/form-data">
<input type="file" name="image" />
</form>The browser automatically changes the request format to:
👉 multipart/form-data
This is a special format designed for:
- Sending files
- Sending form data + files together
Understanding multipart/form-data
1. How Data Is Usually Sent in HTTP Requests
When the browser sends data to the server, it sends it using HTTP.
Every HTTP request has:
- Headers
- Body
The Content-Type header tells the server what kind of data is inside the body.
Example:
If we send JSON:
Content-Type: application/jsonIf we send simple form data (text only):
Content-Type: application/x-www-form-urlencodedBut when we upload files…
We use:
Content-Type: multipart/form-data2. Why JSON Cannot Send Files Properly
JSON works great for text:
{
"name": "Dipika",
"age": 21
}But files like images or PDFs are not simple text.
They are binary data (raw bytes).
If we try to send files inside JSON:
- The file would need to be converted to base64
- That increases size
- It becomes inefficient
- Not practical for large files
So instead of converting the file into text…
The browser sends it in parts.
That's where multipart/form-data comes in.
3. What Does "Multipart" Mean?
The word multipart means:
👉 The request body is divided into multiple parts.
Each part represents:
- A text field OR
- A file
So instead of one big JSON object…
The body looks like multiple sections separated by boundaries.
4. What Actually Happens When You Upload a File?
Suppose we have this form:
<form method="POST" enctype="multipart/form-data">
<input type="text" name="username" />
<input type="file" name="profile" />
</form>When you submit this form:
Step 1:
The browser automatically:
- Sets the header:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXYZ⚠️ IMPORTANT: You should NOT manually set this header when using frontend tools like:
- fetch()
- axios()
- FormData()
Because the browser automatically generates the correct boundary.
If you manually set:
Content-Type: multipart/form-dataWithout boundary ❌
The server cannot understand how parts are separated.
5. What Is Boundary?
This is very important.
A boundary is a unique string used to separate each part of the form data.
Example header:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryABC123Inside the request body, it looks like:
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="username"
Dipika
------WebKitFormBoundaryABC123
Content-Disposition: form-data; name="profile"; filename="photo.png"
Content-Type: image/png
(binary data of image here)
------WebKitFormBoundaryABC123--See what happened?
Each section is separated by:
------WebKitFormBoundaryABC123That is the boundary.
The server reads:
- Start boundary
- Headers of that part
- Data of that part
- Next boundary
- Repeat
That's how it understands different parts.
👉 NEVER manually set
Content-Typewhen sending FormData.
6. How Server Receives Multipart Data
Now comes the backend part.
When Express receives:
multipart/form-dataIt cannot parse it automatically.
Why?
Because:
- The body contains binary streams
- It contains boundaries
- It contains multiple headers inside body
Parsing this manually is complex.
That's why we use:
👉 Multer
Multer:
- Reads the stream
- Detects boundaries
- Extracts each part
- Saves files
- Puts text fields inside
req.body - Puts file info inside
req.fileorreq.files
🔬 How Multer Parses multipart/form-data Internally
1. First Important Thing: Multer Does NOT Parse Everything Alone
Many beginners think Multer does everything itself.
Actually ❌
Multer is built on top of a package called:
👉 Busboy
Busboy is the real engine that parses multipart/form-data.
Multer is like a wrapper around Busboy that makes it easy to use with:
👉 Express
So internally:
Express → Multer → Busboy → Parses multipart stream
2. What Happens When a File Upload Request Comes?
When client sends:
Content-Type: multipart/form-data; boundary=----XYZThe request body is not normal text.
It is a stream.
Important concept:
👉 Files are sent as streams, not as full data at once.
That means:
- Data comes in small chunks
- Not all at once in memory
This is very important for performance.
3. How Multer Intercepts the Request
When you write:
app.post("/upload", upload.single("profile"), (req, res) => {
// your code
});upload.single("profile") is middleware.
So flow becomes:
- Request comes
- Multer middleware runs FIRST
- Multer reads request stream
- Parses multipart data
- Then passes control to your route
4. How Parsing Actually Works Internally
Let's understand the internal process step by step.
Step A: Read Headers
Busboy first reads:
Content-Type: multipart/form-data; boundary=----XYZIt extracts the boundary:
----XYZThis boundary tells Busboy:
👉 Where each part starts and ends.
Step B: Start Reading Stream
The request body is a readable stream.
Busboy listens to it like:
- "data" events
- "file" events
- "field" events
As soon as it detects a boundary:
It knows a new part has started.
Step C: Detect Whether It's a File or Text Field
Each part has something like:
Content-Disposition: form-data; name="username"OR
Content-Disposition: form-data; name="profile"; filename="photo.png"
Content-Type: image/pngIf filename exists → it's a file
If no filename → it's a text field
Busboy checks this internally.
5. What Happens For Text Fields?
If it's text:
Busboy collects the data.
Multer then stores it in:
req.bodySo:
req.body.usernameworks normally.
6. What Happens For Files?
If it's a file:
Busboy creates a file stream.
Now Multer decides what to do with it.
There are two main storage types:
- Disk Storage
- Memory Storage
📦 What Is Storage in Multer?
When a user uploads a file, Multer must decide:
👉 Where should I put this file?
That decision is called Storage.
So storage means:
How and where the uploaded file will be stored.
Multer gives us two main storage options:
- Disk Storage
- Memory Storage
1️⃣ Disk Storage (Save File in a Folder)
This is the most common one.
In this case:
- The file is saved in your project folder
- Example:
/uploadsfolder
So if user uploads:
photo.pngIt gets saved like:
project/
├── uploads/
└── abc123.pngWhat happens internally?
Remember earlier we said files come as streams?
Multer:
- Receives file stream
- Creates a write stream
- Saves file chunk by chunk into a folder
You don't see this happening, but internally it's like:
fileStream → write into file on diskAfter saving, Multer gives you file info in:
req.fileSo you can store the file path in database.
Disk Storage = User uploads file → Multer saves it physically in your computer/server → You get file path
2️⃣ Memory Storage (Store File in RAM)
In this case:
- The file is NOT saved in any folder
- It is stored temporarily in RAM (server memory)
Multer keeps the file as a Buffer inside:
req.file.bufferThat means:
The file exists only in memory while request is running.
After response finishes:
❌ It disappears
Memory Storage = User uploads file → Multer keeps it in memory → You process it → It is gone after request
🚨 Important Question
Why not always use Memory Storage?
Because:
If someone uploads:
- 100MB file
- Many users upload at same time
All files will stay in RAM.
Server may crash ❌
So memory storage is used only when:
- File is small
- You immediately send it to cloud (like S3)
- You process it quickly
Internal Flow Summary
Client uploads file
↓
Browser creates multipart/form-data with boundary
↓
Express receives request
↓
Multer middleware intercepts
↓
Busboy reads stream
↓
Boundary separates parts
↓
Detects file or text
↓
Text → req.body
File → saved (disk/memory)
↓
next() called
↓
Your route runsCreate a file picker on the frontend
A file picker is an input element that lets the user choose a file from their device.
In HTML, this is done using:
<input type="file">This input opens:
- File explorer (Windows / Mac)
- Gallery (mobile)
User selects a file → browser now has access to that file.
Important Attributes of File Input
🔹 name (VERY IMPORTANT)
The name attribute is the key used by backend.
Example:
<input type="file" name="profileImage" />🔹 accept (Optional)
This restricts file type on frontend.
Example:
<input type="file" accept="image/*" />This means:
- User can select only images
⚠️ This is NOT security. It's just for user experience.
Backend validation is still required.
✅ Only PDF
<input type="file" accept="application/pdf">✅ Only DOC & DOCX
<input type="file" accept=".doc,.docx">✅ Images + PDF
<input type="file" accept="image/*,application/pdf">⚠️ IMPORTANT:
acceptis NOT security- Users can still upload other files (by typing
*.*or drag-drop) - Backend validation is mandatory (Multer
fileFilter)
🔹 multiple (Optional)
To allow multiple files:
<input type="file" name="images" multiple />How Files Are Sent Over Network ?
Files are sent as:
- Streams of binary data
- Chunk by chunk
- Not as normal strings
To send such data, HTTP uses:
👉 multipart/form-data
And JSON does NOT support this format.
✅ Solution: FormData
To send files correctly, browsers provide a special object called:
👉 FormData
FormData is designed to:
- Send text data
- Send file data
- Automatically use
multipart/form-data
What Is FormData?
FormData is like a virtual form created using JavaScript.
It works exactly like an HTML form with file inputs.
Example:
const formData = new FormData();Now you can add data to it.
➕ Adding Normal Text to FormData
formData.append("name", "Google");
formData.append("location", "Bangalore");This is similar to:
<input name="name" value="Google" />
<input name="location" value="Bangalore" />➕ Adding File to FormData
formData.append("companyLogo", companyFormData.companyLogo);Here:
companyFormData.companyLogois a File object- Browser knows it is a file
- Browser sends it as binary data
What FormData Looks Like Internally
Internally, the browser converts FormData into:
multipart/form-dataWith:
- Boundaries
- Separate parts
- File metadata
- Binary file content
You don't have to do anything manually.
Installing and Setting Up Multer
Before we upload files, we need a tool that can understand multipart/form-data and handle files.
That tool is Multer.
Multer works with:
- Node.js
- Express
1. What Is Multer
Multer is a middleware.
Middleware means:
Code that runs before your API logic.
Multer's job is to:
- Read
multipart/form-data - Extract files
- Save files (or keep them in memory)
- Make files available as
req.fileorreq.files
Without Multer: ❌ Express cannot read uploaded files.
2. Installing Multer
Inside your backend project folder, run:
npm install multerThat's it.
3. Basic Folder Structure
Before writing code, create a folder to store uploaded files.
project/
├── uploads/
├── routes/
├── controllers/
├── server.js👉 uploads/ is where Multer will save files (disk storage).
4. Create a Separate Multer Config File
backend/
├── config/
│ └── multer.js
├── uploads/
├── routes/
├── controllers/
└── server.js👉 We will write all Multer logic inside config/multer.js.
5. Open config/multer.js
import multer from "multer"Before writing code, understand this sentence 👇
diskStorage means: save uploaded files inside a folder on the server
So Multer needs answers to two questions:
- ❓ WHERE should I save the file?
- ❓ WHAT should be the file name?
That's all.
6. Create Storage
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, "uploads/");
},
filename: function (req, file, cb) {
cb(null, Date.now() + "-" + file.originalname);
},
});🔹multer.diskStorage({})
multer.diskStorag({ .... })🧠 Meaning:
"Multer, I want to store files on disk (folder), not in memory."
🔹 destination (WHERE to save file)
destination: function (req, file, cb) {
cb(null, "uploads/");
}It tells Multer which folder to save the file in.
❓ What is file?
It contains file info like:
- original name
- type (image/png)
- size
❓ What is cb?
cb = callback function
❗ MOST IMPORTANT LINE
cb(null, "uploads/");"There is NO error, and save file inside
uploads/folder."
null→ no error"uploads/"→ folder name
🔹 filename (WHAT name to give file)
filename: function (req, file, cb) {
cb(null, Date.now() + "-" + file.originalname);
}❓ Why do we need this?
If two users upload:
logo.png
logo.pngOne file will overwrite the other ❌
So we make name unique.
❓ file.originalname?
This is the original file name from user's computer.
Example:
logo.png❓ Date.now()?
It gives current timestamp:
1697049123456❗ Final filename becomes:
1697049123456-logo.pngAnd this is what gets saved.
7. Create Multer Upload Middleware
Now connect storage to Multer.
const upload = multer({ storage })
export default upload🧠 Meaning:
"Multer, use THIS storage rule when handling files."
This upload is what we will use in routes.
8. Use This in Route
In your route file:
import upload from "../config/multer.js";Then :
router.post(
"/create",
upload.single("companyLogo"),
createCompany
);🧠 Meaning of upload.single("companyLogo"):
"Accept ONLY ONE file with name
companyLogo."
This must match frontend:
formData.append("companyLogo", file);Upload Files to Cloudinary using Multer
WHY Cloudinary?
Problems with local uploads/ folder:
- Files deleted if server restarts
- Not scalable
- Not CDN optimized
Cloudinary gives:
- ✅ Permanent storage
- ✅ Image optimization
- ✅ CDN URLs
- ✅ Free tier
STEP 1: Create Cloudinary Account
- Go to cloudinary.com
- Sign up (free)
- Open Dashboard
- Copy these 3 values:
Cloud name
API Key
API SecretSTEP 2: Install Cloudinary SDK ( backend)
npm install cloudinarySTEP 3: Setup Cloudinary Config
Create file:
backend/
├── config/
│ ├── multer.js
│ └── cloudinary.jsconfig/cloudinary.js
import { v2 as cloudinary } from "cloudinary";
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
export default cloudinary;STEP 4: Add Environment Variables
In .env:
CLOUDINARY_CLOUD_NAME=xxxx
CLOUDINARY_API_KEY=xxxx
CLOUDINARY_API_SECRET=xxxx⚠️ Never push .env to GitHub
STEP 5: Upload File to Cloudinary (Controller)
Now Multer already gives you:
req.file.pathThat path is the local file, which we send to Cloudinary.
Example: controllers/companyController.js
import cloudinary from "../config/cloudinary.js";
export const createCompany = async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ message: "Logo is required" });
}
// Upload to Cloudinary
const result = await cloudinary.uploader.upload(req.file.path, {
folder: "company_logos",
});
// Cloudinary image URL
const logoUrl = result.secure_url;
res.status(201).json({
message: "Company created",
logo: logoUrl,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
};STEP 6: Route
router.post(
"/create",
upload.single("companyLogo"),
createCompany
);STEP 7: Frontend Must Match
const formData = new FormData();
formData.append("companyLogo", file);🔁 FULL FLOW
Frontend FormData
↓
Multer → saves file temporarily (uploads/)
↓
Controller → uploads file to Cloudinary
↓
Cloudinary → returns image URL
↓
Save URL in databaseDeleting Local files
When using Multer + Cloudinary, the flow is:
Upload → Save temporarily → Upload to Cloudinary → DONEAfter Cloudinary upload:
- Keeping files locally wastes disk space
- Server storage fills quickly
So we delete the local file after successful Cloudinary upload.
Delete Local File After Cloudinary Upload
Node.js provides a built-in module called fs (file system).
- Import fs
import fs from "fs";2. Update Controller example
import cloudinary from "../config/cloudinary.js";
import fs from "fs";
export const createCompany = async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({ message: "Logo is required" });
}
// Upload to Cloudinary
const result = await cloudinary.uploader.upload(req.file.path, {
folder: "company_logos",
});
// Delete local file after upload
fs.unlinkSync(req.file.path);
res.status(201).json({
message: "Company created",
logo: result.secure_url,
});
} catch (error) {
res.status(500).json({ error: error.message });
}
};What fs.unlinkSync() Does
Meaning:
- ❌ Remove file from
uploads/folder - ✅ File still exists safely on Cloudinary
Deleting Files From Cloudinary
Cloudinary does NOT delete by URL. It deletes using public_id.
Example public_id:
company_logos/1697049123456-logoSave public_id in Database
When uploading:
const result = await cloudinary.uploader.upload(req.file.path, {
folder: "company_logos",
});
const logoData = {
url: result.secure_url,
public_id: result.public_id,
};👉 Always store both:
secure_urlpublic_id
Delete Image From Cloudinary
await cloudinary.uploader.destroy(public_id);Example: Delete Company Logo
export const deleteCompanyLogo = async (req, res) => {
try {
const { public_id } = req.body;
await cloudinary.uploader.destroy(public_id);
res.status(200).json({ message: "Image deleted from Cloudinary" });
} catch (error) {
res.status(500).json({ error: error.message });
}
};Adding File Size Limits
Without limits:
- Users can upload 500MB files
- Server can crash
Add Size Limit in Multer Config
config/multer.js
import multer from "multer";
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "uploads/");
},
filename: (req, file, cb) => {
cb(null, Date.now() + "-" + file.originalname);
},
});
const upload = multer({
storage,
limits: {
fileSize: 2 * 1024 * 1024, // 2 MB
},
});
export default upload;