Skip to content
Backend35-50 min

Multer File Upload Implementation

This guide covers single-file and multi-file uploads, storage configuration, MIME validation, file size limits, error handling, and how to return public URLs safely.

Node.jsExpressMulter

Prerequisites

  • An Express API project.
  • A form or client request that sends multipart/form-data.
  • A local uploads folder or cloud storage adapter.
  • A clear list of allowed file types and maximum file sizes.
1

Install Multer

Implementation snippet
npm install multer
npm install -D @types/multer
2

Create an upload folder

  1. Create an `uploads` folder outside sensitive source directories.
  2. Serve it statically only if files are meant to be public.
  3. Add the folder to `.gitignore` so user uploads are not committed.
Implementation snippet
mkdir uploads

# .gitignore
uploads/
!uploads/.gitkeep
3

Configure safe storage

Generate server-side filenames. Do not trust the user's original filename as your stored filename.

Implementation snippet
import multer from "multer";
import path from "node:path";
import crypto from "node:crypto";

const storage = multer.diskStorage({
  destination: "uploads/",
  filename: (_req, file, cb) => {
    const ext = path.extname(file.originalname).toLowerCase();
    cb(null, `${crypto.randomUUID()}${ext}`);
  },
});
4

Validate file type and size

  1. Allow only the MIME types your feature actually needs.
  2. Set a strict file size limit.
  3. Reject unexpected files before writing business records to the database.
Implementation snippet
const allowedTypes = new Set(["image/png", "image/jpeg", "application/pdf"]);

export const upload = multer({
  storage,
  limits: { fileSize: 5 * 1024 * 1024 },
  fileFilter: (_req, file, cb) => {
    if (!allowedTypes.has(file.mimetype)) {
      return cb(new Error("Only PNG, JPG, and PDF files are allowed"));
    }
    cb(null, true);
  },
});
5

Create a single file upload route

  1. Use `upload.single("file")` when the form sends one file.
  2. Check `req.file` before continuing.
  3. Store file metadata in your database if the upload belongs to a user or record.
  4. Return a stable ID or URL to the frontend.
Implementation snippet
app.post("/api/uploads/avatar", requireAuth, upload.single("file"), async (req, res) => {
  if (!req.file) return res.status(400).json({ message: "File is required" });

  const file = await files.create({
    userId: req.user.id,
    filename: req.file.filename,
    originalName: req.file.originalname,
    mimeType: req.file.mimetype,
    size: req.file.size,
    path: req.file.path,
  });

  res.status(201).json({ file });
});
6

Support multiple files

Implementation snippet
app.post("/api/uploads/documents", requireAuth, upload.array("files", 5), async (req, res) => {
  const uploadedFiles = req.files as Express.Multer.File[];
  if (!uploadedFiles?.length) return res.status(400).json({ message: "At least one file is required" });

  const records = await files.createMany(uploadedFiles.map((file) => ({
    userId: req.user.id,
    filename: file.filename,
    originalName: file.originalname,
    mimeType: file.mimetype,
    size: file.size,
    path: file.path,
  })));

  res.status(201).json({ files: records });
});
7

Handle upload errors

  1. Add an Express error handler after upload routes.
  2. Return 413 for size-limit errors.
  3. Return 400 for invalid type errors.
  4. Keep internal filesystem paths out of public error responses.
Implementation snippet
app.use((err, _req, res, next) => {
  if (err instanceof multer.MulterError) {
    return res.status(err.code === "LIMIT_FILE_SIZE" ? 413 : 400).json({ message: err.message });
  }

  if (err.message?.includes("allowed")) {
    return res.status(400).json({ message: err.message });
  }

  next(err);
});
8

Production checklist

Checklist
  • Scan files with antivirus or malware tooling when uploads are shared with other users.
  • Use object storage like S3, R2, or Supabase Storage for production-scale files.
  • Never execute uploaded files.
  • Generate signed URLs for private files.
  • Delete local files when a database transaction fails after upload.

Need implementation help?

Want this built correctly in your codebase?

Send us your stack, repo context, and the feature you need. We will help you implement it cleanly and hand over the working code.

Free scoping callFixed timelineFull source ownership
Get implementation help