File Uploads

Node.js · Lesson 23 of 30 7 min read Updated

Profile photos, PAN card scans for a loan form, assignment PDFs for a college portal: many apps need users to upload files. Files are sent in a special request format that express.json() cannot read, and they are one of the riskiest kinds of input a server accepts. A careless upload route can fill your disk, overwrite files or let an attacker store a dangerous script on your server.

In this lesson you will accept uploads with multer, the standard Express middleware for this job, save them with safe random names, limit their size and type, show clear error messages and serve the files back.

How browsers send files

A normal form sends name=Ananya&city=Kochi. A form with a file uses the multipart/form-data format instead: the body is split into parts, one per field, and each part can hold text or the raw bytes of a file with its name and type. In HTML you ask for it with enctype:

HTML
<form action="/api/profile-photo" method="post" enctype="multipart/form-data">
  <label>Name <input name="name" required></label>
  <label>Photo <input type="file" name="photo" accept="image/jpeg,image/png,image/webp" required></label>
  <button>Upload</button>
</form>

The name of the file input (photo) is the field name your server will look for. The accept attribute only filters the file picker; it is not a security check. From JavaScript you build the same request with FormData:

JavaScript
const form = new FormData();
form.append("name", "Ananya");
form.append("photo", fileInput.files[0]); // a File chosen by the user
await fetch("/api/profile-photo", { method: "POST", body: form });

Do not set the Content-Type header yourself when sending FormData; fetch() adds it with the required boundary string.

Saving uploads with multer

Install multer (version 2 or newer, which fixed security problems found in 1.x):

Terminal
npm install multer

Multer is middleware. You configure where files go, what they are called, and limits, then add it to the routes that accept files:

JavaScript
import express from "express";
import multer from "multer";
import path from "node:path";
import { randomUUID } from "node:crypto";

const app = express();
const UPLOAD_DIR = path.join(import.meta.dirname, "uploads"); // create this folder first

const ALLOWED = new Map([["image/jpeg", ".jpg"], ["image/png", ".png"], ["image/webp", ".webp"]]);

const storage = multer.diskStorage({
  destination: UPLOAD_DIR,
  filename: (req, file, cb) => {
    // never trust the original name: make our own
    cb(null, `${randomUUID()}${ALLOWED.get(file.mimetype)}`);
  },
});

const upload = multer({
  storage,
  limits: { fileSize: 2 * 1024 * 1024, files: 1 }, // 2 MB, one file
  fileFilter: (req, file, cb) => {
    if (ALLOWED.has(file.mimetype)) cb(null, true);
    else cb(new multer.MulterError("LIMIT_UNEXPECTED_FILE", file.fieldname));
  },
});

app.use("/uploads", express.static(UPLOAD_DIR));

app.post("/api/profile-photo", upload.single("photo"), (req, res) => {
  if (!req.file) return res.status(400).json({ error: "Choose a photo to upload" });
  res.status(201).json({
    name: req.body.name,
    originalName: req.file.originalname,
    size: req.file.size,
    type: req.file.mimetype,
    url: `/uploads/${req.file.filename}`,
  });
});

app.use((err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    const messages = {
      LIMIT_FILE_SIZE: "File is too large",
      LIMIT_UNEXPECTED_FILE: "This file type is not allowed here",
    };
    return res.status(400).json({ error: messages[err.code] ?? err.message });
  }
  next(err);
});

app.listen(3000);

What each part does:

  • upload.single("photo") reads one file from the field photo. The file's details go in req.file, and the normal text fields of the form go in req.body.
  • diskStorage saves files to a folder. The filename function chooses the stored name. Here it is a random UUID (a 36-character unique ID) plus an extension chosen from the allowed type, so two users uploading photo.jpg never overwrite each other, and a name such as ../../app.js or shell.php can never reach the disk.
  • limits.fileSize stops the upload as soon as it passes 2 MB, and limits.files caps the number of files. Without limits, one request could fill your disk.
  • fileFilter accepts or rejects each file before it is saved.
  • express.static makes the saved files available at /uploads/<name>.
  • The error handler turns multer's errors, which have a code such as LIMIT_FILE_SIZE, into friendly 400 responses.

Testing the route

This script uploads a valid photo, a PDF, a 3 MB image and nothing at all:

JavaScript
async function send(fileName, type, bytes) {
  const form = new FormData();
  form.append("name", "Ananya");
  if (fileName) form.append("photo", new Blob([new Uint8Array(bytes)], { type }), fileName);
  const res = await fetch("http://localhost:3000/api/profile-photo", { method: "POST", body: form });
  console.log(res.status, await res.json());
}

await send("ananya-graduation.JPG", "image/jpeg", 150_000);
await send("notes.pdf", "application/pdf", 10_000);
await send("huge.png", "image/png", 3 * 1024 * 1024);
await send(null);
Output
201 {
  name: 'Ananya',
  originalName: 'ananya-graduation.JPG',
  size: 150000,
  type: 'image/jpeg',
  url: '/uploads/e8af5475-d3e5-4adb-8bac-e7b67245998f.jpg'
}
400 { error: 'This file type is not allowed here' }
400 { error: 'File is too large' }
400 { error: 'Choose a photo to upload' }

When the size limit is hit, multer deletes the partly written file, so rejected uploads do not pile up. Store the URL (or the file name) in your database row for the user, not the file itself.

Other multer options

CodeUse
upload.single("photo")One file in req.file
upload.array("photos", 5)Up to 5 files from one field, in req.files (an array)
upload.fields([{ name: "front", maxCount: 1 }, { name: "back", maxCount: 1 }])Several named fields, such as both sides of an ID card
upload.none()A multipart form with text fields only
multer({ storage: multer.memoryStorage() })Keep the file in memory as req.file.buffer, for example to resize it or send it to cloud storage

Use memory storage only with a small fileSize limit; ten users uploading 50 MB each would use 500 MB of RAM.

Upload security checklist

  • Limit size and count with limits.
  • Allow a short list of types. The mimetype comes from the client and can be faked, so for important uploads also check the file's first bytes (its "magic number") with a library such as file-type, or process images with a library such as sharp, which fails on non-images.
  • Generate your own file names. Never use originalname as a path.
  • Store uploads outside your code folder, and never execute or import them.
  • Serve private files through a route that checks permissions, not express.static. A marksheet or ID card should only be visible to its owner.
  • For large apps, use object storage such as Amazon S3, Cloudflare R2 or Google Cloud Storage instead of the server's disk, so files survive server rebuilds and can be shared by several servers.

Common mistakes

  • Forgetting enctype="multipart/form-data" on the HTML form. The server then receives only the file name, not the file.
  • Field name mismatch. upload.single("photo") with <input name="image"> gives LIMIT_UNEXPECTED_FILE or an empty req.file.
  • Setting Content-Type by hand with FormData. The boundary is missing and parsing fails.
  • Saving with the original name. Files get overwritten and path tricks become possible.
  • No size limit. One request can fill the disk.

Key points

  • Files arrive as multipart/form-data; multer parses them, putting files in req.file or req.files and text in req.body.
  • Configure diskStorage with a random file name, limits for size and count, and a fileFilter for allowed types.
  • Handle multer.MulterError codes such as LIMIT_FILE_SIZE in an error handler and send clear 400 responses.
  • Do not trust the client's file name or type; serve private files only after a permission check.
  • Use multer 2.x; consider object storage for production apps.

Exercise

Write a route POST /api/assignments for a college portal that accepts up to 3 PDF files from the field files (each at most 5 MB) plus a text field rollNo. Save them with random names ending in .pdf and reply with the roll number and a list of the stored file names and sizes. Reject other file types with a 400 error.

Show solution
JavaScript
const pdfUpload = multer({
  storage: multer.diskStorage({
    destination: UPLOAD_DIR,
    filename: (req, file, cb) => cb(null, `${randomUUID()}.pdf`),
  }),
  limits: { fileSize: 5 * 1024 * 1024, files: 3 },
  fileFilter: (req, file, cb) => {
    if (file.mimetype === "application/pdf") cb(null, true);
    else cb(new multer.MulterError("LIMIT_UNEXPECTED_FILE", file.fieldname));
  },
});

app.post("/api/assignments", pdfUpload.array("files", 3), (req, res) => {
  if (!req.files?.length) return res.status(400).json({ error: "Attach at least one PDF" });
  res.status(201).json({
    rollNo: req.body.rollNo,
    files: req.files.map((f) => ({ storedAs: f.filename, size: f.size })),
  });
});
Output
201 {
  rollNo: '21CS044',
  files: [
    {
      storedAs: '4d1c38d4-4c77-4fc2-8ebf-4cdfdad603bb.pdf',
      size: 48213
    },
    {
      storedAs: '4c3249d6-2912-4860-ad2d-d8120c02d19f.pdf',
      size: 102400
    }
  ]
}

pdfUpload.array("files", 3) accepts up to three files from one field and puts them in req.files. The multer error handler from the lesson turns a fourth file or a non-PDF into 400 { error: 'This file type is not allowed here' }. Your random names will differ.

Quick Quiz

1. Which form attribute is required to upload files?

2. Where does multer put the uploaded file's details when you use upload.single("photo")?

3. Why should you generate your own file names instead of using file.originalname?

4. Why is checking file.mimetype alone not enough for important uploads?

Learn Node.js with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call