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/multer2
Create an upload folder
- Create an `uploads` folder outside sensitive source directories.
- Serve it statically only if files are meant to be public.
- Add the folder to `.gitignore` so user uploads are not committed.
Implementation snippet
mkdir uploads
# .gitignore
uploads/
!uploads/.gitkeep3
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
- Allow only the MIME types your feature actually needs.
- Set a strict file size limit.
- 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
- Use `upload.single("file")` when the form sends one file.
- Check `req.file` before continuing.
- Store file metadata in your database if the upload belongs to a user or record.
- 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
- Add an Express error handler after upload routes.
- Return 413 for size-limit errors.
- Return 400 for invalid type errors.
- 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.