File uploads require careful handling for security and scalability. This guide covers patterns for safe, efficient file processing.
Basic Upload with Multer#
1import multer from 'multer';
2import path from 'path';
3
4// Configure storage
5const storage = multer.diskStorage({
6 destination: './uploads',
7 filename: (req, file, cb) => {
8 const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
9 cb(null, uniqueSuffix + path.extname(file.originalname));
10 },
11});
12
13// File filter
14const fileFilter = (req, file, cb) => {
15 const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
16
17 if (allowedTypes.includes(file.mimetype)) {
18 cb(null, true);
19 } else {
20 cb(new Error('Invalid file type'), false);
21 }
22};
23
24const upload = multer({
25 storage,
26 fileFilter,
27 limits: {
28 fileSize: 5 * 1024 * 1024, // 5MB
29 files: 5, // Max 5 files
30 },
31});
32
33// Routes
34app.post('/upload', upload.single('file'), (req, res) => {
35 res.json({ filename: req.file?.filename });
36});
37
38app.post('/upload-multiple', upload.array('files', 5), (req, res) => {
39 res.json({ files: req.files?.map(f => f.filename) });
40});Upload to S3#
1import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
2import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
3import multer from 'multer';
4
5const s3 = new S3Client({ region: process.env.AWS_REGION });
6
7// Memory storage for S3 upload
8const upload = multer({
9 storage: multer.memoryStorage(),
10 limits: { fileSize: 10 * 1024 * 1024 },
11});
12
13app.post('/upload', upload.single('file'), async (req, res) => {
14 if (!req.file) {
15 return res.status(400).json({ error: 'No file provided' });
16 }
17
18 const key = `uploads/${Date.now()}-${req.file.originalname}`;
19
20 await s3.send(new PutObjectCommand({
21 Bucket: process.env.S3_BUCKET,
22 Key: key,
23 Body: req.file.buffer,
24 ContentType: req.file.mimetype,
25 }));
26
27 res.json({
28 key,
29 url: `https://${process.env.S3_BUCKET}.s3.amazonaws.com/${key}`,
30 });
31});Presigned URLs for Direct Upload#
1// Generate presigned URL for client-side upload
2app.post('/upload/presign', async (req, res) => {
3 const { filename, contentType } = req.body;
4
5 const key = `uploads/${Date.now()}-${filename}`;
6
7 const command = new PutObjectCommand({
8 Bucket: process.env.S3_BUCKET,
9 Key: key,
10 ContentType: contentType,
11 });
12
13 const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 3600 });
14
15 res.json({
16 uploadUrl,
17 key,
18 expiresIn: 3600,
19 });
20});
21
22// Client-side direct upload
23async function uploadFile(file: File) {
24 // Get presigned URL
25 const { uploadUrl, key } = await fetch('/upload/presign', {
26 method: 'POST',
27 headers: { 'Content-Type': 'application/json' },
28 body: JSON.stringify({
29 filename: file.name,
30 contentType: file.type,
31 }),
32 }).then(r => r.json());
33
34 // Upload directly to S3
35 await fetch(uploadUrl, {
36 method: 'PUT',
37 body: file,
38 headers: { 'Content-Type': file.type },
39 });
40
41 return key;
42}File Validation#
1import fileType from 'file-type';
2import sharp from 'sharp';
3
4async function validateImage(buffer: Buffer): Promise<void> {
5 // Check magic bytes, not just extension
6 const type = await fileType.fromBuffer(buffer);
7
8 if (!type || !['image/jpeg', 'image/png', 'image/webp'].includes(type.mime)) {
9 throw new Error('Invalid image type');
10 }
11
12 // Validate image dimensions
13 const metadata = await sharp(buffer).metadata();
14
15 if (!metadata.width || !metadata.height) {
16 throw new Error('Invalid image');
17 }
18
19 if (metadata.width > 4096 || metadata.height > 4096) {
20 throw new Error('Image too large (max 4096x4096)');
21 }
22}
23
24// Virus scanning (using ClamAV)
25import NodeClam from 'clamscan';
26
27const clam = await new NodeClam().init({
28 clamdscan: {
29 host: 'localhost',
30 port: 3310,
31 },
32});
33
34async function scanFile(filePath: string): Promise<boolean> {
35 const { isInfected } = await clam.scanFile(filePath);
36 return !isInfected;
37}Image Processing#
1import sharp from 'sharp';
2
3async function processImage(buffer: Buffer): Promise<{
4 original: Buffer;
5 thumbnail: Buffer;
6 medium: Buffer;
7}> {
8 const image = sharp(buffer);
9
10 const [original, thumbnail, medium] = await Promise.all([
11 // Optimize original
12 image
13 .clone()
14 .webp({ quality: 85 })
15 .toBuffer(),
16
17 // Create thumbnail
18 image
19 .clone()
20 .resize(150, 150, { fit: 'cover' })
21 .webp({ quality: 80 })
22 .toBuffer(),
23
24 // Create medium size
25 image
26 .clone()
27 .resize(800, 800, { fit: 'inside', withoutEnlargement: true })
28 .webp({ quality: 85 })
29 .toBuffer(),
30 ]);
31
32 return { original, thumbnail, medium };
33}
34
35app.post('/upload/image', upload.single('image'), async (req, res) => {
36 const variants = await processImage(req.file.buffer);
37
38 // Upload all variants
39 const baseKey = `images/${Date.now()}`;
40
41 await Promise.all([
42 uploadToS3(`${baseKey}/original.webp`, variants.original),
43 uploadToS3(`${baseKey}/thumbnail.webp`, variants.thumbnail),
44 uploadToS3(`${baseKey}/medium.webp`, variants.medium),
45 ]);
46
47 res.json({
48 original: `${baseKey}/original.webp`,
49 thumbnail: `${baseKey}/thumbnail.webp`,
50 medium: `${baseKey}/medium.webp`,
51 });
52});Chunked Uploads#
1// Initialize multipart upload
2app.post('/upload/init', async (req, res) => {
3 const { filename, contentType } = req.body;
4 const key = `uploads/${Date.now()}-${filename}`;
5
6 const { UploadId } = await s3.send(new CreateMultipartUploadCommand({
7 Bucket: process.env.S3_BUCKET,
8 Key: key,
9 ContentType: contentType,
10 }));
11
12 res.json({ uploadId: UploadId, key });
13});
14
15// Upload chunk
16app.post('/upload/chunk', async (req, res) => {
17 const { uploadId, key, partNumber } = req.body;
18
19 const command = new UploadPartCommand({
20 Bucket: process.env.S3_BUCKET,
21 Key: key,
22 UploadId: uploadId,
23 PartNumber: partNumber,
24 });
25
26 const presignedUrl = await getSignedUrl(s3, command, { expiresIn: 3600 });
27
28 res.json({ presignedUrl });
29});
30
31// Complete upload
32app.post('/upload/complete', async (req, res) => {
33 const { uploadId, key, parts } = req.body;
34
35 await s3.send(new CompleteMultipartUploadCommand({
36 Bucket: process.env.S3_BUCKET,
37 Key: key,
38 UploadId: uploadId,
39 MultipartUpload: { Parts: parts },
40 }));
41
42 res.json({ key });
43});Frontend Upload with Progress#
1function FileUploader() {
2 const [progress, setProgress] = useState(0);
3
4 const uploadFile = async (file: File) => {
5 const { uploadUrl, key } = await getPresignedUrl(file);
6
7 await new Promise((resolve, reject) => {
8 const xhr = new XMLHttpRequest();
9
10 xhr.upload.addEventListener('progress', (e) => {
11 if (e.lengthComputable) {
12 setProgress(Math.round((e.loaded / e.total) * 100));
13 }
14 });
15
16 xhr.addEventListener('load', () => {
17 if (xhr.status === 200) resolve(key);
18 else reject(new Error('Upload failed'));
19 });
20
21 xhr.open('PUT', uploadUrl);
22 xhr.setRequestHeader('Content-Type', file.type);
23 xhr.send(file);
24 });
25 };
26
27 return (
28 <div>
29 <input type="file" onChange={(e) => uploadFile(e.target.files[0])} />
30 {progress > 0 && <ProgressBar value={progress} />}
31 </div>
32 );
33}Security Checklist#
- ✅ Validate file type by magic bytes, not extension
- ✅ Limit file size
- ✅ Generate random filenames
- ✅ Store files outside web root
- ✅ Scan for viruses
- ✅ Validate image dimensions
- ✅ Set proper content-type headers
- ✅ Use presigned URLs for direct upload
Conclusion#
File uploads require validation, secure storage, and efficient processing. Use presigned URLs for large files, validate thoroughly before processing, and store files in object storage like S3 for scalability.