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
- Understanding File Upload Basics
- What is Multer?
- Multer Storage Types
- Basic Multer Setup
- File Validation with Multer
- What is Cloudinary?
- Cloudinary Setup
- Uploading a File to Cloudinary
- Combining Multer + Cloudinary
- Storing the File URL in a Database
- Handling Upload Errors
- Folder Structure (Best Practice)
Understanding File Upload Basics
When a user uploads a file, the general flow looks like this:
Frontend → Backend → Storage (Cloudinary)
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
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) orreq.files(multiple) - Temporarily store or buffer files before sending them to cloud storage
Install it with:
npm install multer
Multer Storage Types
Multer gives you two storage options:
Disk Storage
Saves the file directly to your local filesystem:
uploads/
image.png
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");
});
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);
}
},
});
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
This keeps your backend stateless and your media scalable.
Cloudinary Setup
Install the Cloudinary Node.js SDK:
npm install cloudinary
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;
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);
});
};
The response from Cloudinary includes:
{
secure_url: "https://res.cloudinary.com/...",
public_id: "my-app/abc123",
// ...other metadata
}
You store secure_url in your database.
Note: Install
streamifierwithnpm install streamifierto convert a buffer into a readable stream.
Combining Multer + Cloudinary
Here's the complete 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
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 });
}
};
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"
}
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" });
});
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
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_streamto 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)