Difficulty: Intermediate
How do you handle file uploads in a Node.js application? Explain Multer configuration and S3 integration.
File uploads in Express use the Multer middleware, which processes multipart/form-data (the encoding type for file uploads). Multer can store files to disk or keep them in memory as Buffer objects.
Memory storage is preferred when uploading to cloud services like S3 because the file stays in RAM as a buffer and can be streamed directly to S3 without writing to disk first. Disk storage is useful when files are served locally.
AWS S3 is the standard cloud storage solution. The upload flow is: client sends file -> Multer parses it into req.file -> your code uploads the buffer to S3 -> S3 returns a URL -> you store the URL in the database.
File validation is critical: check file type (MIME type and magic bytes), file size, and file name. Never trust the client-provided MIME type alone. Validate the actual file content by reading magic bytes to prevent malicious file uploads.
const multer = require('multer');
const path = require('path');
// Memory storage (for S3 uploads)
const memoryUpload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB max
fileFilter: (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only JPEG, PNG, and WebP allowed.'));
}
},
});
// Disk storage (for local files)
const diskUpload = multer({
storage: multer.diskStorage({
destination: (req, file, cb) => cb(null, 'uploads/'),
filename: (req, file, cb) => {
const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1e9)}`;
cb(null, uniqueName + path.extname(file.originalname));
},
}),
limits: { fileSize: 10 * 1024 * 1024 },
});
// Route usage
app.post('/api/upload/avatar',
memoryUpload.single('avatar'), // single file, field name 'avatar'
uploadController.uploadAvatar
);
app.post('/api/upload/documents',
diskUpload.array('files', 5), // up to 5 files, field name 'files'
uploadController.uploadDocuments
);
memoryStorage keeps files as Buffer in req.file.buffer (ideal for S3). diskStorage writes to local filesystem. fileFilter validates before accepting.
const { S3Client, PutObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
const crypto = require('crypto');
const s3 = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
async function uploadToS3(file, folder = 'uploads') {
const fileExtension = path.extname(file.originalname);
const key = `${folder}/${crypto.randomUUID()}${fileExtension}`;
await s3.send(new PutObjectCommand({
Bucket: process.env.AWS_BUCKET_NAME,
Key: key,
Body: file.buffer,
ContentType: file.mimetype,
}));
return `https://${process.env.AWS_BUCKET_NAME}.s3.${process.env.AWS_REGION}.amazonaws.com/${key}`;
}
async function deleteFromS3(url) {
const key = new URL(url).pathname.slice(1);
await s3.send(new DeleteObjectCommand({
Bucket: process.env.AWS_BUCKET_NAME,
Key: key,
}));
}
// Controller
async function uploadAvatar(req, res) {
if (!req.file) return res.status(400).json({ error: 'No file' });
const url = await uploadToS3(req.file, 'avatars');
await prisma.user.update({
where: { id: req.user.id },
data: { avatar: url },
});
res.json({ url });
}
Generate unique keys with randomUUID to prevent collisions. Store the S3 URL in the database. Delete old files when updating.
// Validate actual file content, not just MIME type
function validateFileContent(buffer, declaredMime) {
const signatures = {
'image/jpeg': [0xFF, 0xD8, 0xFF],
'image/png': [0x89, 0x50, 0x4E, 0x47],
'image/webp': null, // Check RIFF header
'application/pdf': [0x25, 0x50, 0x44, 0x46], // %PDF
};
const expected = signatures[declaredMime];
if (!expected) return false;
// Compare magic bytes
for (let i = 0; i < expected.length; i++) {
if (buffer[i] !== expected[i]) return false;
}
return true;
}
// Usage in upload middleware
function validateUpload(req, res, next) {
if (!req.file) return next();
const isValid = validateFileContent(
req.file.buffer,
req.file.mimetype
);
if (!isValid) {
return res.status(400).json({
error: 'File content does not match declared type'
});
}
next();
}
app.post('/api/upload',
memoryUpload.single('file'),
validateUpload,
uploadController.handle
);
Magic bytes are the first few bytes of a file that identify its type. Checking them prevents users from uploading malicious files renamed to .jpg.
Multer, AWS S3, File Validation, Memory Storage, Disk Storage