File Uploads
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:
<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:
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):
npm install multerMulter is middleware. You configure where files go, what they are called, and limits, then add it to the routes that accept files:
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 fieldphoto. The file's details go inreq.file, and the normal text fields of the form go inreq.body.diskStoragesaves files to a folder. Thefilenamefunction 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 uploadingphoto.jpgnever overwrite each other, and a name such as../../app.jsorshell.phpcan never reach the disk.limits.fileSizestops the upload as soon as it passes 2 MB, andlimits.filescaps the number of files. Without limits, one request could fill your disk.fileFilteraccepts or rejects each file before it is saved.express.staticmakes the saved files available at/uploads/<name>.- The error handler turns multer's errors, which have a
codesuch asLIMIT_FILE_SIZE, into friendly400responses.
Testing the route
This script uploads a valid photo, a PDF, a 3 MB image and nothing at all:
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);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
| Code | Use |
|---|---|
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
mimetypecomes 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 asfile-type, or process images with a library such assharp, which fails on non-images. - Generate your own file names. Never use
originalnameas a path. - Store uploads outside your code folder, and never execute or
importthem. - 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">givesLIMIT_UNEXPECTED_FILEor an emptyreq.file. - Setting
Content-Typeby hand withFormData. 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 inreq.fileorreq.filesand text inreq.body. - Configure
diskStoragewith a random file name,limitsfor size and count, and afileFilterfor allowed types. - Handle
multer.MulterErrorcodes such asLIMIT_FILE_SIZEin an error handler and send clear400responses. - 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
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 })),
});
});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?
Without the multipart encoding, the browser sends only the file name.
2. Where does multer put the uploaded file's details when you use upload.single("photo")?
single() stores one file in req.file; text fields go in req.body.
3. Why should you generate your own file names instead of using file.originalname?
Random names avoid collisions and keep attacker-controlled text out of file paths.
4. Why is checking file.mimetype alone not enough for important uploads?
MIME types are claims from the browser or script; verify the bytes for sensitive 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
