Chapter 4.2☕ 20 min read

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.

01What is File Upload? — Biryani Photo Upload Kaise Kaam Karta Hai

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:

  1. Client creates a form with <input type="file">
  2. Form uses enctype="multipart/form-data" — a special encoding for files
  3. Client sends a POST request with the file data
  4. Server receives the file and saves it to disk or cloud storage
  5. Server stores the file path/URL in database
  6. 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.

02multer Setup — File Upload Middleware Install Karna

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

FieldDescriptionExample
fieldnameField name from formbiryaniPhoto
originalnameOriginal file name from clientchicken-biryani.jpg
encodingFile encoding7bit
mimetypeFile MIME typeimage/jpeg
destinationWhere file was saveduploads/
filenameGenerated filenamebiryani-1700000000-123456789.jpg
pathFull path to fileuploads/biryani-17000...jpg
sizeFile size in bytes245760
03Disk Storage vs Memory Storage — File Kahaan Save Karein

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?

FeatureDisk StorageMemory Storage
Use caseLocal file storageCloud upload, processing
File accessreq.file.pathreq.file.buffer
Memory usageLow (written to disk)High (kept in RAM)
Best forImages, documentsSmall files, cloud upload
Cloud integrationRead file, then uploadDirect 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.

04File Validation — Sirf Images Allowed! .exe nahi chalega

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
05Complete Upload Flow — Photo Upload karo, URL Store karo

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:

  1. Restaurant opens a form with <input type="file" name="biryaniPhoto">
  2. Form sends POST /api/menu with enctype="multipart/form-data"
  3. Multer middleware intercepts, validates file type + size
  4. If valid: Multer saves file to uploads/ folder with unique name
  5. Route handler saves biryani details (name, price, photoUrl) to MongoDB
  6. Server returns biryani data with photo URL
  7. 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.jpg

Key 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)
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase