When we build a backend using Node.js and Express, we usually handle normal data like:

  • name
  • email
  • 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.body

2️⃣ 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/json

If we send simple form data (text only):

Content-Type: application/x-www-form-urlencoded

But when we upload files…

We use:

Content-Type: multipart/form-data

2. 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-data

Without 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=----WebKitFormBoundaryABC123

Inside 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:

------WebKitFormBoundaryABC123

That 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-Type when sending FormData.

6. How Server Receives Multipart Data

Now comes the backend part.

When Express receives:

multipart/form-data

It 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.file or req.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=----XYZ

The 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:

  1. Request comes
  2. Multer middleware runs FIRST
  3. Multer reads request stream
  4. Parses multipart data
  5. 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=----XYZ

It extracts the boundary:

----XYZ

This 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/png

If 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.body

So:

req.body.username

works 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:

  1. Disk Storage
  2. 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:

  1. Disk Storage
  2. 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: /uploads folder

So if user uploads:

photo.png

It gets saved like:

project/
 ├── uploads/
      └── abc123.png

What 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 disk

After saving, Multer gives you file info in:

req.file

So 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.buffer

That 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 runs

Create 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:

  • accept is 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.companyLogo is 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-data

With:

  • 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.file or req.files

Without Multer: ❌ Express cannot read uploaded files.

2. Installing Multer

Inside your backend project folder, run:

npm install multer

That'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:

  1. ❓ WHERE should I save the file?
  2. ❓ 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.png

One 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.png

And 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

  1. Go to cloudinary.com
  2. Sign up (free)
  3. Open Dashboard
  4. Copy these 3 values:
Cloud name
API Key
API Secret

STEP 2: Install Cloudinary SDK ( backend)

npm install cloudinary

STEP 3: Setup Cloudinary Config

Create file:

backend/
 ├── config/
 │    ├── multer.js
 │    └── cloudinary.js

config/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.path

That 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 database

Deleting Local files

When using Multer + Cloudinary, the flow is:

Upload → Save temporarily → Upload to Cloudinary → DONE

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

  1. 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-logo

Save 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_url
  • public_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;