Local File Uploads
Handle file uploads with Multer, Formidable, and Busboy - choose the right tool for your needs.
6 min read
File Uploads in Express#
HTML forms can send files. Your API needs to receive them, validate them, and store them. There are several libraries to help with this.
Option 1: Multer (Most Popular)#
Multer is the go-to middleware for handling multipart/form-data:
bash
npm install multer
Basic Setup#
javascript
// src/middleware/upload.js
import multer from 'multer';
import path from 'path';
import crypto from 'crypto';
// Storage configuration
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
// Generate unique filename
const uniqueName = crypto.randomBytes(16).toString('hex');
const ext = path.extname(file.originalname);
cb(null, `${uniqueName}${ext}`);
},
});
// File filter
const fileFilter = (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only JPEG, PNG, GIF, and WebP allowed.'), false);
}
};
// Create multer instance
export const upload = multer({
storage,
fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
files: 5, // Max 5 files
},
});
Single File Upload#
javascript
// src/routes/uploads.js
import { Router } from 'express';
import { upload } from '../middleware/upload.js';
const router = Router();
// Single file
router.post('/avatar', upload.single('avatar'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
res.json({
data: {
filename: req.file.filename,
path: req.file.path,
size: req.file.size,
mimetype: req.file.mimetype,
},
});
});
export default router;
Multiple Files#
javascript
// Multiple files with same field name
router.post('/gallery', upload.array('photos', 10), (req, res) => {
res.json({
data: {
count: req.files.length,
files: req.files.map(f => ({
filename: f.filename,
size: f.size,
})),
},
});
});
// Multiple fields with different names
router.post('/document', upload.fields([
{ name: 'document', maxCount: 1 },
{ name: 'thumbnail', maxCount: 1 },
]), (req, res) => {
res.json({
data: {
document: req.files['document']?.[0],
thumbnail: req.files['thumbnail']?.[0],
},
});
});
Memory Storage (For Cloud Uploads)#
javascript
// Store in memory instead of disk
const memoryStorage = multer.memoryStorage();
export const uploadToMemory = multer({
storage: memoryStorage,
limits: { fileSize: 5 * 1024 * 1024 },
});
// File available as buffer
router.post('/upload', uploadToMemory.single('file'), async (req, res) => {
const buffer = req.file.buffer; // File contents in memory
// Upload to cloud storage
const url = await cloudStorage.upload(buffer, req.file.originalname);
res.json({ data: { url } });
});
Option 2: Formidable#
Lower-level, more control:
bash
npm install formidable
javascript
import formidable from 'formidable';
import path from 'path';
router.post('/upload', async (req, res) => {
const form = formidable({
uploadDir: './uploads',
keepExtensions: true,
maxFileSize: 5 * 1024 * 1024,
filter: ({ mimetype }) => {
return mimetype && mimetype.includes('image');
},
});
try {
const [fields, files] = await form.parse(req);
const uploadedFile = files.file?.[0];
if (!uploadedFile) {
return res.status(400).json({ error: 'No file uploaded' });
}
res.json({
data: {
filename: path.basename(uploadedFile.filepath),
size: uploadedFile.size,
type: uploadedFile.mimetype,
},
});
} catch (error) {
res.status(400).json({ error: error.message });
}
});
Option 3: Busboy (Streaming)#
Best for large files - streams directly without buffering:
bash
npm install busboy
javascript
import Busboy from 'busboy';
import fs from 'fs';
import path from 'path';
router.post('/upload', (req, res) => {
const busboy = Busboy({
headers: req.headers,
limits: {
fileSize: 50 * 1024 * 1024, // 50MB
},
});
const uploads = [];
busboy.on('file', (name, file, info) => {
const { filename, mimeType } = info;
const savePath = path.join('uploads', `${Date.now()}-${filename}`);
const writeStream = fs.createWriteStream(savePath);
file.pipe(writeStream);
file.on('limit', () => {
fs.unlinkSync(savePath); // Delete if too large
});
writeStream.on('finish', () => {
uploads.push({ filename, path: savePath });
});
});
busboy.on('finish', () => {
res.json({ data: { files: uploads } });
});
busboy.on('error', (error) => {
res.status(500).json({ error: error.message });
});
req.pipe(busboy);
});
Comparison#
| Feature | Multer | Formidable | Busboy |
|---|---|---|---|
| Ease of use | Easiest | Medium | Harder |
| Streaming | Limited | Yes | Best |
| Memory usage | Higher | Medium | Lowest |
| Express integration | Built-in | Manual | Manual |
| Large files | OK | Good | Best |
| Best for | Most cases | Control | Large files |
File Validation#
Validate File Type#
javascript
import { fileTypeFromBuffer } from 'file-type';
async function validateFileType(buffer, allowedTypes) {
const type = await fileTypeFromBuffer(buffer);
if (!type || !allowedTypes.includes(type.mime)) {
throw new Error('Invalid file type');
}
return type;
}
// Usage with memory storage
router.post('/upload', uploadToMemory.single('file'), async (req, res) => {
try {
await validateFileType(req.file.buffer, ['image/jpeg', 'image/png']);
// Proceed with upload
} catch (error) {
res.status(400).json({ error: error.message });
}
});
Validate Image Dimensions#
bash
npm install sharp
javascript
import sharp from 'sharp';
async function validateImageDimensions(buffer, maxWidth, maxHeight) {
const metadata = await sharp(buffer).metadata();
if (metadata.width > maxWidth || metadata.height > maxHeight) {
throw new Error(`Image too large. Max ${maxWidth}x${maxHeight}`);
}
return metadata;
}
Processing Images#
Resize with Sharp#
javascript
import sharp from 'sharp';
router.post('/avatar', uploadToMemory.single('avatar'), async (req, res) => {
// Resize and convert
const processed = await sharp(req.file.buffer)
.resize(200, 200, {
fit: 'cover',
position: 'center',
})
.jpeg({ quality: 80 })
.toBuffer();
// Save processed file
const filename = `avatar-${Date.now()}.jpg`;
await fs.promises.writeFile(`uploads/${filename}`, processed);
res.json({ data: { filename } });
});
Generate Thumbnails#
javascript
async function processImage(buffer, filename) {
const baseName = path.parse(filename).name;
// Original (optimized)
const original = await sharp(buffer)
.jpeg({ quality: 85 })
.toFile(`uploads/${baseName}.jpg`);
// Thumbnail
const thumbnail = await sharp(buffer)
.resize(150, 150, { fit: 'cover' })
.jpeg({ quality: 70 })
.toFile(`uploads/${baseName}-thumb.jpg`);
// Medium
const medium = await sharp(buffer)
.resize(800, 600, { fit: 'inside', withoutEnlargement: true })
.jpeg({ quality: 80 })
.toFile(`uploads/${baseName}-medium.jpg`);
return {
original: `${baseName}.jpg`,
thumbnail: `${baseName}-thumb.jpg`,
medium: `${baseName}-medium.jpg`,
};
}
Serving Uploaded Files#
javascript
import express from 'express';
import path from 'path';
// Static file serving
app.use('/uploads', express.static('uploads'));
// Or with authentication
app.get('/files/:filename', authenticate, (req, res) => {
const filePath = path.join('uploads', req.params.filename);
// Check if user has access to this file
if (!userCanAccessFile(req.user, req.params.filename)) {
return res.status(403).json({ error: 'Forbidden' });
}
res.sendFile(path.resolve(filePath));
});
Error Handling for Uploads#
javascript
// Multer error handler
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' });
}
if (err.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({ error: 'Too many files' });
}
if (err.code === 'LIMIT_UNEXPECTED_FILE') {
return res.status(400).json({ error: 'Unexpected field' });
}
}
next(err);
});
Key Takeaways#
- Multer for most cases - Easy, well-documented, Express integrated
- Busboy for large files - Streaming prevents memory issues
- Always validate - File type, size, dimensions
- Process images - Resize, optimize, generate thumbnails
- Secure access - Don't expose uploads directory directly
Security
Never trust the file extension. Validate actual file contents. A malicious user can rename virus.exe to image.jpg. Use file-type library to check magic bytes.
Continue Learning
Ready to level up your skills?
Explore more guides and tutorials to deepen your understanding and become a better developer.