DEV Community

Cover image for Day 45 of #100DaysOfCode — File Upload with Multer and Cloudinary
M Saad Ahmad
M Saad Ahmad

Posted on

Day 45 of #100DaysOfCode — File Upload with Multer and Cloudinary

Every app we use daily: Instagram, Twitter, Google Drive, WhatsApp, has one thing in common: they all let you upload files. Profile pictures, documents, videos, etc.

While it may seem like a simple task on the frontend, handling file uploads on the backend involves understanding how data is transmitted, processed, validated, and stored efficiently. File uploads are one of those things that feel complicated until you understand the flow.

On day 45, the goal was to understand how the backend receives the file uploaded by the user on the frontend and processes it using Multer before letting Cloudinary save it in cloud storage and return a URL we can store in the database.


TL;DR

  • Files are sent as multipart/form-data, not JSON
  • Multer processes the file on the backend
  • Cloudinary stores it in the cloud and gives you a URL
  • You save that URL in your database — not the file itself

Table of Contents

  1. Understanding File Upload Basics
  2. What is Multer?
  3. Multer Storage Types
  4. Basic Multer Setup
  5. File Validation with Multer
  6. What is Cloudinary?
  7. Cloudinary Setup
  8. Uploading a File to Cloudinary
  9. Combining Multer + Cloudinary
  10. Storing the File URL in a Database
  11. Handling Upload Errors
  12. Folder Structure (Best Practice)

Understanding File Upload Basics

When a user uploads a file, the general flow looks like this:

Frontend → Backend → Storage (Cloudinary)
Enter fullscreen mode Exit fullscreen mode

The key thing to understand is that files are not sent as JSON. They are sent as multipart/form-data — a special encoding type that allows binary data (like images) to travel over HTTP.

Content-Type: multipart/form-data
Enter fullscreen mode Exit fullscreen mode

Your backend needs special middleware (Multer) to parse this format.


What is Multer?

Multer is an Express middleware for handling multipart/form-data — which is exactly the format used for file uploads.

It helps you:

  • Parse incoming file data
  • Access uploaded files via req.file (single) or req.files (multiple)
  • Temporarily store or buffer files before sending them to cloud storage

Install it with:

npm install multer
Enter fullscreen mode Exit fullscreen mode

Multer Storage Types

Multer gives you two storage options:

Disk Storage

Saves the file directly to your local filesystem:

uploads/
  image.png
Enter fullscreen mode Exit fullscreen mode

This is useful for local development but not recommended in production — your server's disk isn't a reliable or scalable place to store user files permanently.

Memory Storage

Stores the file in RAM as a Buffer object. This is the right choice when you plan to immediately forward the file to a cloud service like Cloudinary.

👉 For Cloudinary → always use memory storage


Basic Multer Setup

Here's what a basic Multer setup looks like:

import multer from "multer";

const storage = multer.memoryStorage();
const upload = multer({ storage });

app.post("/upload", upload.single("image"), (req, res) => {
  console.log(req.file); // file info + buffer
  res.send("File received");
});
Enter fullscreen mode Exit fullscreen mode

Key concepts:

  • upload.single("image") — the field name ("image") must match the key used in the frontend form
  • req.file — contains the file metadata and the buffer (in memory storage)
  • It's used as a middleware in the route, before your controller logic

File Validation with Multer

Validating files before processing them is important for both security and UX.

You can configure Multer with a fileFilter and limits:

const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 2 * 1024 * 1024 }, // 2MB max
  fileFilter: (req, file, cb) => {
    const allowed = ["image/jpeg", "image/png"];
    if (allowed.includes(file.mimetype)) {
      cb(null, true);
    } else {
      cb(new Error("Only JPG and PNG files are allowed"), false);
    }
  },
});
Enter fullscreen mode Exit fullscreen mode

Things to validate:

  • File type — only allow images (check mimetype, not just the extension)
  • File size — reject files that are too large before they reach Cloudinary

What is Cloudinary?

Cloudinary is a cloud-based media management service. It handles:

  • Storing images and videos
  • Optimizing and transforming media on-the-fly
  • Generating publicly accessible URLs

Instead of saving a file on your server, the pattern is:

Upload → Cloudinary → Get URL → Save URL in DB
Enter fullscreen mode Exit fullscreen mode

This keeps your backend stateless and your media scalable.


Cloudinary Setup

Install the Cloudinary Node.js SDK:

npm install cloudinary
Enter fullscreen mode Exit fullscreen mode

Then configure it using your account credentials (store these in .env):

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;
Enter fullscreen mode Exit fullscreen mode

You'll find these credentials by signing up on Cloudinary.


Uploading a File to Cloudinary

When using memory storage, Multer gives you a Buffer (not a file path). Cloudinary's uploader.upload() accepts a file path or base64 string — so for a buffer, you either convert it or use upload_stream:

import streamifier from "streamifier";

const uploadToCloudinary = (buffer) => {
  return new Promise((resolve, reject) => {
    const stream = cloudinary.uploader.upload_stream(
      { folder: "my-app" },
      (error, result) => {
        if (error) reject(error);
        else resolve(result);
      }
    );
    streamifier.createReadStream(buffer).pipe(stream);
  });
};
Enter fullscreen mode Exit fullscreen mode

The response from Cloudinary includes:

{
  secure_url: "https://res.cloudinary.com/...",
  public_id: "my-app/abc123",
  // ...other metadata
}
Enter fullscreen mode Exit fullscreen mode

You store secure_url in your database.

Note: Install streamifier with npm install streamifier to convert a buffer into a readable stream.


Combining Multer + Cloudinary

Here's the complete upload flow:

File Upload Flow

Client uploads file
       ↓
Multer processes the file (memoryStorage → req.file.buffer)
       ↓
Send buffer to Cloudinary via upload_stream
       ↓
Cloudinary returns secure_url
       ↓
Save secure_url in Database
Enter fullscreen mode Exit fullscreen mode

A full controller example:

export const uploadFile = async (req, res) => {
  try {
    const result = await uploadToCloudinary(req.file.buffer);
    res.status(200).json({ url: result.secure_url });
  } catch (error) {
    res.status(500).json({ message: "Upload failed", error });
  }
};
Enter fullscreen mode Exit fullscreen mode

Storing the File URL in a Database

You never save the actual file in the database. You save the URL that Cloudinary gives you.

Example MongoDB document:

{
  name: "Product Name",
  image: "https://res.cloudinary.com/your-cloud/image/upload/v123/my-app/abc.jpg"
}
Enter fullscreen mode Exit fullscreen mode

This way:

  • Your database stays lightweight
  • Images are served via Cloudinary's CDN (fast globally)
  • You can apply transformations via URL parameters later

Handling Upload Errors

Common upload errors you should handle:

Error Cause Fix
File too large Exceeds limits.fileSize Show size limit message
Wrong format Invalid mimetype Reject in fileFilter
Cloud upload fails Network/auth issue Catch in try/catch, retry or notify user

Always:

  • Validate the file before sending it to Cloudinary
  • Use error-handling middleware in Express for consistent error responses
app.use((err, req, res, next) => {
  if (err.message === "Only JPG and PNG files are allowed") {
    return res.status(400).json({ message: err.message });
  }
  res.status(500).json({ message: "Something went wrong" });
});
Enter fullscreen mode Exit fullscreen mode

Folder Structure (Best Practice)

Keeping concerns separated makes the code maintainable:

src/
├── config/
│   └── cloudinary.js        # Cloudinary config
├── middleware/
│   └── upload.js            # Multer setup
├── controllers/
│   └── uploadController.js  # Upload logic
└── routes/
    └── uploadRoutes.js      # Route definitions
Enter fullscreen mode Exit fullscreen mode

This follows the separation of concerns principle — each file has one clear responsibility.


Key Takeaways

  • Files travel as multipart/form-data — JSON won't work for uploads
  • Use memory storage in Multer when uploading to Cloudinary
  • Use upload_stream to pipe a buffer to Cloudinary
  • Never store the file in your DB — store the secure_url
  • Always validate file type and size before processing

File uploads feel tricky at first, but once you see the flow — Multer → Cloudinary → URL → DB — it makes total sense.

Thanks for reading. Feel free to share your thoughts!

Top comments (0)