File Upload — Biryani Photo Upload with Multer
File upload in web apps is like taking a photo of your biryani and sharing it on Swiggy. Multer handles the messy part of receiving files from HTML forms.
File upload is how users send files (images, PDFs, videos) from their browser to your server. Think of it like uploading your biryani photo to Swiggy's menu or submitting a profile picture on a food delivery app.
How file upload works:
- Client creates a form with
<input type="file"> - Form uses
enctype="multipart/form-data"— a special encoding for files - Client sends a POST request with the file data
- Server receives the file and saves it to disk or cloud storage
- Server stores the file path/URL in database
- Server returns the URL so the client can display the image
Why multer? In Express, express.json() handles JSON bodies, but it cannot handle file uploads. Files use multipart/form-data encoding. Multer is middleware that parses this encoding and makes the file available.
Without multer, you would have to manually parse the raw multipart data — parsing boundaries, extracting binary data, writing to disk — it's a nightmare. Multer handles all of this for you.
In this chapter, you'll build a biryani photo upload system where restaurants can upload photos of their biryani dishes. The photos get saved to the server, and the URL is stored in MongoDB for the menu.
Multer is a Node.js middleware for handling multipart/form-data — which is the encoding used for file uploads in HTML forms.
npm install multer
Basic multer setup:
// middleware/upload.middleware.js — Multer Setup
import multer from 'multer';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Configure storage
const storage = multer.diskStorage({
destination: (req, file, cb) => {
// Save files to 'uploads/' folder
cb(null, path.join(__dirname, '../uploads'));
},
filename: (req, file, cb) => {
// Create unique filename: biryani-{timestamp}-{random}.{ext}
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
cb(null, 'biryani-' + uniqueSuffix + ext);
}
});
// Create multer instance with storage config
const upload = multer({ storage });
export default upload;
Using multer in routes:
// routes/upload.routes.js — File Upload Routes
import express from 'express';
import upload from '../middleware/upload.middleware.js';
import { protect } from '../middleware/auth.middleware.js';
const router = express.Router();
// POST /api/upload — Upload a single file
router.post('/', protect, upload.single('biryaniPhoto'), async (req, res) => {
try {
if (!req.file) {
return res.status(400).json({
success: false,
message: 'No file uploaded! Koi photo toh bhejo bhai! 📸'
});
}
// req.file contains the uploaded file info
const fileUrl = '/uploads/' + req.file.filename;
res.json({
success: true,
message: 'Biryani photo uploaded successfully! 🎉',
data: {
filename: req.file.filename,
originalName: req.file.originalname,
size: req.file.size,
mimetype: req.file.mimetype,
url: fileUrl
}
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Upload failed: ' + error.message
});
}
});
// POST /api/upload/multiple — Upload multiple files
router.post('/multiple', protect, upload.array('biryaniPhotos', 5), async (req, res) => {
try {
if (!req.files || req.files.length === 0) {
return res.status(400).json({ success: false, message: 'No files uploaded!' });
}
const files = req.files.map(file => ({
filename: file.filename,
originalName: file.originalname,
size: file.size,
mimetype: file.mimetype,
url: '/uploads/' + file.filename
}));
res.json({
success: true,
message: files.length + ' photos uploaded successfully! 🎉',
data: files
});
} catch (error) {
res.status(500).json({ success: false, message: 'Upload failed: ' + error.message });
}
});
export default router;
The req.file object (after multer processes it):
| Field | Description | Example |
|---|---|---|
fieldname | Field name from form | biryaniPhoto |
originalname | Original file name from client | chicken-biryani.jpg |
encoding | File encoding | 7bit |
mimetype | File MIME type | image/jpeg |
destination | Where file was saved | uploads/ |
filename | Generated filename | biryani-1700000000-123456789.jpg |
path | Full path to file | uploads/biryani-17000...jpg |
size | File size in bytes | 245760 |
Multer supports two storage options: disk storage (saves to filesystem) and memory storage (keeps file in RAM as buffer). Choose based on your use case.
Disk Storage (diskStorage):
// Disk Storage — File directly saved to disk
// Best for: Images, PDFs, videos — any file you want to keep
import multer from 'multer';
const diskStorage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/'); // Save to uploads folder
},
filename: (req, file, cb) => {
// biryani-1234567890.jpg
cb(null, 'biryani-' + Date.now() + path.extname(file.originalname));
}
});
const uploadDisk = multer({ storage: diskStorage });
// Usage: uploadDisk.single('photo')
Memory Storage:
// Memory Storage — File kept in RAM as buffer
// Best for: Small files you want to process immediately or upload to cloud (S3, Cloudinary)
import multer from 'multer';
const storage = multer.memoryStorage();
const uploadMemory = multer({
storage,
limits: { fileSize: 5 * 1024 * 1024 } // 5MB limit
});
// req.file.buffer contains the file data
// Use it to upload to cloud storage:
// await cloudinary.uploader.upload_stream(req.file.buffer);
// await s3.upload({ Body: req.file.buffer }).promise();
When to use which?
| Feature | Disk Storage | Memory Storage |
|---|---|---|
| Use case | Local file storage | Cloud upload, processing |
| File access | req.file.path | req.file.buffer |
| Memory usage | Low (written to disk) | High (kept in RAM) |
| Best for | Images, documents | Small files, cloud upload |
| Cloud integration | Read file, then upload | Direct upload from buffer |
Important: In production, you typically use memory storage to upload directly to cloud storage (AWS S3, Cloudinary, Google Cloud Storage). Disk storage is mainly for development and simple use cases.
Never trust user-uploaded files blindly! Users might upload malicious files (like .exe files pretending to be biryani photos). Always validate file types and sizes.
File type validation:
// middleware/upload.middleware.js — With File Validation
import multer from 'multer';
import path from 'path';
// Allowed file types for biryani photos
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, 'biryani-' + uniqueSuffix + path.extname(file.originalname));
}
});
// File filter function
const fileFilter = (req, file, cb) => {
// Check MIME type
if (ALLOWED_TYPES.includes(file.mimetype)) {
// Accept file
cb(null, true);
} else {
// Reject file — send error
cb(new Error('Sirf images allowed hain bhai! JPEG, PNG, WebP, ya GIF daalo. Ye ' + file.mimetype + ' kya bhej diya? 🚫'), false);
}
};
const upload = multer({
storage,
fileFilter,
limits: {
fileSize: MAX_FILE_SIZE // 5MB limit
}
});
export default upload;
Handling multer errors in routes:
// routes/upload.routes.js — Error Handling for Multer
import multer from 'multer';
router.post('/', (req, res) => {
upload.single('biryaniPhoto')(req, res, (err) => {
if (err) {
// Multer-specific errors
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({
success: false,
message: 'File too large! Max 5MB allowed — itni badi photo mat bhejo bhai! 📏'
});
}
return res.status(400).json({
success: false,
message: 'Upload error: ' + err.message
});
}
// Custom file filter errors
return res.status(400).json({
success: false,
message: err.message
});
}
if (!req.file) {
return res.status(400).json({
success: false,
message: 'No file selected! Koi photo toh chuno bhai! 📸'
});
}
// File uploaded successfully
res.json({
success: true,
data: {
url: '/uploads/' + req.file.filename,
size: req.file.size,
type: req.file.mimetype
}
});
});
});
Common validation checks:
- File type (MIME check) — Only allow images (JPEG, PNG, WebP, GIF). Block .exe, .js, .html, .svg (can contain scripts)
- File size — Limit uploads to 5MB. Large files can crash your server or increase costs
- Filename sanitization — Remove special characters to prevent path traversal attacks (e.g.,
../../../etc/passwd) - Magic bytes verification — For security-critical apps, verify file content (not just extension/MIME) using file-type library
Let's put it all together — a complete biryani photo upload and menu management system.
// controllers/menu.controller.js — Complete Menu with Photo Upload
import Biryani from '../models/Biryani.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// POST /api/menu — Add a new biryani with photo
export const addBiryani = async (req, res) => {
try {
const { name, price, category, description } = req.body;
const file = req.file;
if (!file) {
return res.status(400).json({
success: false,
message: 'Biryani photo required! Photo ke bina menu kaise banega? 📸'
});
}
const biryani = await Biryani.create({
name,
price,
category,
description,
photoUrl: '/uploads/' + file.filename,
restaurant: req.user.id
});
res.status(201).json({
success: true,
message: name + ' menu mein add ho gaya! 🎉',
data: biryani
});
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
};
// GET /api/menu — Get all biryanis with photos
export const getAllBiryanis = async (req, res) => {
try {
const biryanis = await Biryani.find().populate('restaurant', 'name');
res.json({
success: true,
count: biryanis.length,
data: biryanis.map(b => ({
...b.toObject(),
photoUrl: b.photoUrl ? 'http://localhost:3000' + b.photoUrl : null
}))
});
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
};
// DELETE /api/menu/:id — Delete biryani (and its photo)
export const deleteBiryani = async (req, res) => {
try {
const biryani = await Biryani.findById(req.params.id);
if (!biryani) {
return res.status(404).json({ success: false, message: 'Biryani not found!' });
}
// Delete the photo file from disk
const filePath = path.join(__dirname, '..', biryani.photoUrl);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath); // Delete physical file
}
await biryani.deleteOne();
res.json({ success: true, message: 'Biryani deleted with photo! 🗑️' });
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
};
// models/Biryani.js — Biryani Schema with Photo
import mongoose from 'mongoose';
const biryaniSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Biryani ka name daalo bhai!'],
trim: true
},
price: {
type: Number,
required: [true, 'Price daalo — free mein thodi degi restaurant?'],
min: [10, 'Itni sasti biryani nahi hoti bhai!']
},
category: {
type: String,
enum: ['Chicken', 'Mutton', 'Veg', 'Fish', 'Beef'],
required: true
},
description: {
type: String,
default: 'Hyderabadi style biryani — full masaledar!'
},
photoUrl: {
type: String,
default: ''
},
restaurant: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
isAvailable: {
type: Boolean,
default: true
}
}, { timestamps: true });
export default mongoose.model('Biryani', biryaniSchema);
Complete upload flow:
- Restaurant opens a form with
<input type="file" name="biryaniPhoto"> - Form sends POST /api/menu with
enctype="multipart/form-data" - Multer middleware intercepts, validates file type + size
- If valid: Multer saves file to
uploads/folder with unique name - Route handler saves biryani details (name, price, photoUrl) to MongoDB
- Server returns biryani data with photo URL
- Client displays the photo:
<img src="/uploads/biryani-1700000000.jpg">
Important: Serve static files!
// server.js — Serve uploaded files so clients can access them
import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Serve uploads folder as static files
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
// Now clients can access: http://localhost:3000/uploads/biryani-1700000000.jpgKey Takeaways
- ✅ File upload uses multipart/form-data encoding — different from JSON
- ✅ Multer is Express middleware that handles file upload parsing
- ✅ multer.diskStorage() saves files to disk; multer.memoryStorage() keeps in buffer (for cloud upload)
- ✅ req.file contains uploaded file info: filename, mimetype, size, path, buffer
- ✅ Always validate file types (MIME check) — block .exe, .js from being uploaded
- ✅ Always limit file size: limits: { fileSize: 5 * 1024 * 1024 }
- ✅ Use unique filenames (timestamp + random) to prevent overwrites and guessing
- ✅ Serve uploaded files with express.static middleware
- ✅ Memory storage is best for production with cloud storage (S3, Cloudinary)
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login