I’ve spent years building web applications, and one challenge consistently arises: handling file uploads efficiently. Every time I implement an upload feature, I’m reminded how critical it’s become in modern web development. From profile pictures to document sharing, users expect seamless file handling. But doing it right? That requires careful planning. Today, I’ll share how to build a robust upload system using Node.js tools that scale.
When starting, proper setup prevents headaches later. Here’s how I initialize my projects:
npm init -y
npm install express multer sharp aws-sdk helmet joi uuid
My tsconfig.json
ensures TypeScript behaves predictably:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"strict": true
}
}
Environment variables keep secrets safe. I store them in .env
and access via:
// config.ts
export const CONFIG = {
AWS_BUCKET_NAME: process.env.AWS_BUCKET_NAME,
MAX_FILE_SIZE: parseInt(process.env.MAX_FILE_SIZE || '10485760'),
ALLOWED_MIME_TYPES: ['image/jpeg', 'image/png']
};
Multer handles incoming files. Notice how I validate types and sizes:
const storage = multer.memoryStorage();
const upload = multer({
storage,
fileFilter: (req, file, cb) => {
if (!CONFIG.ALLOWED_MIME_TYPES.includes(file.mimetype)) {
return cb(new Error('Invalid file type'));
}
cb(null, true);
},
limits: { fileSize: CONFIG.MAX_FILE_SIZE }
});
Why accept files in memory? Because we need to process them before storage. But what about very large files? We’ll address that later.
For images, Sharp transforms them efficiently. I generate multiple sizes in parallel:
async function processImage(buffer: Buffer) {
const thumbnail = sharp(buffer).resize(150, 150).toFormat('webp');
const medium = sharp(buffer).resize(500, 500).toFormat('webp');
const [thumbBuf, mediumBuf] = await Promise.all([
thumbnail.toBuffer(),
medium.toBuffer()
]);
return { thumbnail: thumbBuf, medium: mediumBuf };
}
WebP format typically reduces file sizes by 30% compared to JPEG - significant savings when serving thousands of images.
Now to cloud storage. AWS S3 offers durability, but we must configure it properly:
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({ region: CONFIG.AWS_REGION });
async function uploadToS3(buffer: Buffer, key: string) {
const command = new PutObjectCommand({
Bucket: CONFIG.AWS_BUCKET_NAME,
Key: key,
Body: buffer,
ContentType: 'image/webp'
});
return s3.send(command);
}
For large files, chunked uploads prevent timeouts. The @aws-sdk/lib-storage
package simplifies this:
import { Upload } from '@aws-sdk/lib-storage';
const upload = new Upload({
client: s3,
params: { Bucket: 'my-bucket', Key: 'largefile.zip', Body: stream },
partSize: 10 * 1024 * 1024 // 10MB chunks
});
upload.on('httpUploadProgress', (progress) => {
console.log(`Uploaded ${progress.loaded} bytes`);
});
Security can’t be an afterthought. I always implement:
- Rate limiting:
express-rate-limit
prevents abuse - MIME validation: Reject unexpected file types
- Virus scanning: Integrate ClamAV or commercial services
- Temporary URLs: Generate expiring S3 links for downloads
app.use(rateLimit({
windowMs: 15 * 60 * 1000,
max: 100 // 100 requests per IP
}));
Performance optimization often involves moving work out of the main thread. I offload image processing to worker threads using Node’s worker_threads
module. For monitoring, Prometheus metrics help track:
- Upload success rates
- Processing times
- S3 error rates
Testing is crucial. I use Jest for unit tests and Supertest for API endpoints:
test('rejects oversized files', async () => {
const res = await request(app)
.post('/upload')
.attach('file', Buffer.alloc(CONFIG.MAX_FILE_SIZE + 1), 'test.jpg');
expect(res.statusCode).toBe(413);
});
Before deployment, I always:
- Set up proper S3 bucket policies
- Enable CloudFront for faster global delivery
- Configure auto-scaling based on queue length
- Implement comprehensive logging
I remember when a client’s social platform gained sudden traction - our upload system handled 50,000 files daily without breaking. That’s the power of these tools combined.
What would happen if we skipped MIME validation? I once saw an attacker upload executable files disguised as images. Scary, right? That’s why every layer of security matters.
Now it’s your turn. Try implementing resumable uploads for extra credit. If this guide helped, share it with others facing similar challenges. What file handling issues have you encountered? Let me know in the comments!