3.7 — Handling Files with Express: Quick Revision
Episode 3 supplement -- print-friendly.
How to use
Skim -> drill weak spots in 3.7.a through 3.7.f -> 3.7-Exercise-Questions.md.
Why Multer?
express.json() handles application/json -- cannot handle files
express.urlencoded() handles application/x-www-form-urlencoded -- cannot handle files
- Files require
multipart/form-data -- only Multer (or similar) can parse it
- HTML forms need
enctype="multipart/form-data" for file inputs
Install and Basic Setup
npm install multer
const express = require('express');
const multer = require('multer');
const app = express();
const upload = multer({ dest: 'uploads/' });
app.use(express.json());
app.post('/upload', upload.single('avatar'), (req, res) => {
console.log(req.file);
console.log(req.body);
res.json({ file: req.file });
});
Upload Methods
| Method | Usage | req.file | req.files |
|---|
upload.single('name') | One file | File object | -- |
upload.array('name', max) | Multiple, same field | -- | Array |
upload.fields([...]) | Multiple, different fields | -- | Object of arrays |
upload.none() | Text-only multipart | -- | -- |
upload.any() | Any field (avoid in prod) | -- | Array |
Storage Engines
Disk Storage
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, 'uploads/'),
filename: (req, file, cb) => cb(null, Date.now() + '-' + file.originalname)
});
const upload = multer({ storage });
- File saved to disk immediately
- Access via
req.file.path and req.file.filename
- Good for: local processing, permanent storage on server
Memory Storage
const upload = multer({ storage: multer.memoryStorage() });
- File held as Buffer in RAM
- Access via
req.file.buffer
- Good for: cloud upload pipeline, magic bytes validation
- Risk: memory exhaustion with large files
When to Use Which
| Criteria | Disk | Memory |
|---|
| File touches disk? | Yes | No |
| Available as Buffer? | No (read manually) | Yes (req.file.buffer) |
| Cloud upload? | Upload from file path | Stream buffer directly |
| Large files? | Safe | Risky (RAM) |
| Temp cleanup needed? | Yes | No |
req.file Properties Table
| Property | Disk Storage | Memory Storage | Description |
|---|
fieldname | Yes | Yes | Form field name |
originalname | Yes | Yes | Original filename from client |
encoding | Yes | Yes | File encoding (e.g., 7bit) |
mimetype | Yes | Yes | MIME type (e.g., image/jpeg) |
destination | Yes | -- | Upload folder path |
filename | Yes | -- | Generated filename on disk |
path | Yes | -- | Full path to saved file |
size | Yes | Yes | File size in bytes |
buffer | -- | Yes | File data as Buffer |
File Filter (Validation)
const imageFilter = (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Only images are allowed'), false);
}
};
const upload = multer({
storage: multer.memoryStorage(),
fileFilter: imageFilter,
limits: { fileSize: 5 * 1024 * 1024 }
});
Size Limits
limits: {
fileSize: 5 * 1024 * 1024,
files: 10,
fields: 20,
fieldSize: 1024 * 1024
}
Error Handling
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'File too large' });
}
return res.status(400).json({ error: err.message });
}
if (err) return res.status(400).json({ error: err.message });
next();
});
| Error Code | Cause |
|---|
LIMIT_FILE_SIZE | File exceeds limits.fileSize |
LIMIT_FILE_COUNT | Too many files |
LIMIT_UNEXPECTED_FILE | Field name mismatch |
LIMIT_FIELD_COUNT | Too many text fields |
Magic Bytes Validation
const fileType = require('file-type');
const result = await fileType.fromBuffer(req.file.buffer);
if (!result || !['image/jpeg', 'image/png'].includes(result.mime)) {
return res.status(400).json({ error: 'Invalid file type' });
}
| Format | Magic Bytes |
|---|
| JPEG | FF D8 FF |
| PNG | 89 50 4E 47 |
| PDF | 25 50 44 46 |
| GIF | 47 49 46 38 |
Security Checklist
Cloud Upload Pattern (Cloudinary)
Configuration
const cloudinary = require('cloudinary').v2;
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET
});
Upload Buffer to Cloudinary
function uploadToCloudinary(buffer, folder) {
return new Promise((resolve, reject) => {
cloudinary.uploader.upload_stream(
{ folder, resource_type: 'auto' },
(error, result) => error ? reject(error) : resolve(result)
).end(buffer);
});
}
const result = await uploadToCloudinary(req.file.buffer, 'avatars');
Delete from Cloudinary
await cloudinary.uploader.destroy(publicId);
URL Transformations
https://res.cloudinary.com/CLOUD/image/upload/w_200,h_200,c_fill,g_face,q_auto,f_auto/PUBLIC_ID.jpg
| Param | Effect |
|---|
w_200 | Width 200px |
h_200 | Height 200px |
c_fill | Crop to fill dimensions |
g_face | Center on detected face |
q_auto | Auto quality |
f_auto | Auto format (WebP/JPEG) |
Production Pipeline
Client (FormData) -> Multer (memory) -> Validate (MIME + magic bytes)
-> Cloudinary (upload_stream) -> Save URL to MongoDB -> Return URL to client
Key rule: The database stores the URL, never the file. The server never permanently stores the file on disk.
One-Liners
- Multer = middleware that parses
multipart/form-data for file uploads.
- Disk storage = files on filesystem. Memory storage = files as Buffers in RAM.
fileFilter = accept/reject files before storage. limits = cap size and count.
- Magic bytes = first bytes of a file reveal its true type. More reliable than MIME.
- Cloud storage = upload to Cloudinary/S3; store URL in DB; serve via CDN.
End of 3.7 quick revision.