File Uploads and Storage
Why File Uploads Require Special Handling
Standard HTTP requests send data as JSON or URL-encoded text. Files — images, PDFs, videos — are binary data and must be sent using multipart/form-data encoding. Unlike JSON, multipart requests can contain multiple parts with different content types in a single request.
Your Express server cannot parse multipart data with its built-in body parser. You need a dedicated library.
Multer: Handling Multipart File Uploads
Multer is the standard middleware for handling multipart/form-data in Express.
npm install multer
Basic Setup — Disk Storage
const express = require('express');
const multer = require('multer');
const path = require('path');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/'); // folder must exist
},
filename: (req, file, cb) => {
const ext = path.extname(file.originalname);
const filename = Date.now() + '-' + Math.round(Math.random() * 1e9) + ext;
cb(null, filename);
},
});
const upload = multer({ storage });
const app = express();
// Single file upload
app.post('/upload', upload.single('avatar'), (req, res) => {
if (!req.file) {
return res.status(400).json({ success: false, message: 'No file uploaded' });
}
res.status(200).json({
success: true,
message: 'File uploaded successfully',
data: { filename: req.file.filename, path: req.file.path },
});
});
The upload.single('avatar') middleware processes one file from a form field named avatar and attaches it to req.file.
For multiple files use upload.array('photos', 5) — up to 5 files from a field named photos. They are available at req.files.
Validating File Type and Size
Accepting any file without validation is dangerous. A malicious user could upload a .php script as if it were an image.
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE_MB = 5;
const fileFilter = (req, file, cb) => {
if (!ALLOWED_TYPES.includes(file.mimetype)) {
return cb(new Error('Only JPEG, PNG, and WebP images are allowed'), false);
}
cb(null, true);
};
const upload = multer({
storage,
fileFilter,
limits: { fileSize: MAX_SIZE_MB * 1024 * 1024 }, // bytes
});
When Multer rejects a file, it passes an error to Express's error handler. Always handle MulterError specifically:
// In your centralised error handler
if (err.name === 'MulterError') {
return res.status(400).json({ success: false, message: err.message });
}
Local Storage vs Cloud Storage
Storing files on the same server as your application is simple but has major drawbacks:
- Files are lost if the server restarts or crashes
- Horizontal scaling (multiple servers) means files only exist on one server
- Disk space is limited
Cloud storage solves all of these. The two most popular options are:
| Provider | Service | Free Tier |
|---|---|---|
| Cloudinary | Image and video management | 25 credits/month |
| Amazon Web Services | S3 (Simple Storage Service) | 5 GB for 12 months |
Uploading to Cloudinary
npm install cloudinary multer-storage-cloudinary
const cloudinary = require('cloudinary').v2;
const { CloudinaryStorage } = require('multer-storage-cloudinary');
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET,
});
const cloudStorage = new CloudinaryStorage({
cloudinary,
params: {
folder: 'careerex-uploads',
allowed_formats: ['jpg', 'png', 'webp'],
transformation: [{ width: 800, crop: 'limit' }], // resize on upload
},
});
const cloudUpload = multer({ storage: cloudStorage });
app.post('/upload/avatar', cloudUpload.single('avatar'), (req, res) => {
res.status(200).json({
success: true,
message: 'Avatar uploaded',
data: {
url: req.file.path, // Cloudinary CDN URL
publicId: req.file.filename, // used to delete later
},
});
});
The returned url is a full CDN (Content Delivery Network) URL like https://res.cloudinary.com/your-cloud/image/upload/.... Store this URL in your database alongside the user record.
Deleting Files from Cloudinary
When a user replaces their avatar, delete the old file to avoid storage waste:
const deleteOldAvatar = async (publicId) => {
if (publicId) {
await cloudinary.uploader.destroy(publicId);
}
};
app.put('/users/:id/avatar', cloudUpload.single('avatar'), asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) throw new AppError('User not found', 404);
await deleteOldAvatar(user.avatarPublicId);
user.avatarUrl = req.file.path;
user.avatarPublicId = req.file.filename;
await user.save();
res.status(200).json({ success: true, data: user });
}));
Serving Local Files
If you use local disk storage during development, serve the uploads directory as static files:
app.use('/uploads', express.static('uploads'));
Files are then accessible at http://localhost:5000/uploads/filename.jpg.
Practice Exercise
- Install Multer and create an
/api/v1/uploadendpoint that accepts a single image (JPEG or PNG only, max 2 MB). - Store uploaded files in an
uploads/directory locally. - Return the file URL in the response envelope.
- Add error handling for oversized files and invalid file types.
- (Bonus) Configure Cloudinary storage and replace local storage.
Try it yourself
Key Takeaways
- File uploads require multipart/form-data encoding; Multer is the standard middleware to handle this in Express.
- Always validate file type (mimetype) and file size before saving — never trust the file extension alone.
- Local disk storage is only suitable for development; use Cloudinary or AWS S3 (Simple Storage Service) in production.
- Store the cloud file URL and public ID in your database so you can display and later delete the file.
- Handle Multer errors specifically in your centralised error handler, returning a 400 status with a clear message.
Quick Quiz
1.Why must file uploads use multipart/form-data encoding instead of JSON?
2.What is the main advantage of using cloud storage (like Cloudinary or S3) over local disk storage?
3.What does Multer's fileFilter function do?
4.After a user replaces their profile picture, why should you delete the old file from Cloudinary?
Ready to go further?
CareerEx gives you structured 12-week training, live classes every Saturday and Sunday, real tutor feedback, and a certificate. Join the next cohort.
Join CareerEx