File Uploads & Media Processing
15 questions foundUpload progress tracking primarily happens client-side (using the browser's XMLHttpRequest upload.onprogress event or fetch with a ReadableStream), since the browser has direct visibility into how much of the request body has actually been sent -- server-side, you can additionally track how many bytes have been received so far (useful for logging or a websocket-pushed progress update to other connected clients), typically by listening to data events on the raw incoming request stream before it's fully parsed by multer.
// Client-side (the primary mechanism for progress)
const xhr = new XMLHttpRequest();
xhr.upload.onprogress = (event) => {
const percent = (event.loaded / event.total) * 100;
updateProgressBar(percent);
};
xhr.open('POST', '/upload');
xhr.send(formData);
Real-world example A file-upload UI shows a real-time progress bar using the browser's XMLHttpRequest upload progress event, giving users clear feedback during a large file transfer, entirely independent of anything the Node.js server needs to do differently to support it.
Common follow-ups: Why doesn't the more modern fetch() API have a built-in equivalent to XMLHttpRequest's upload.onprogress out of the box?;How would you push server-side processing progress (like 'transcoding: 40% complete') back to the client after the upload itself has finished?
HTTP & Web Servers;WebSockets & Real-Time Communication
A common approach integrates with a virus-scanning engine like ClamAV, running either as a local daemon the Node.js process communicates with over a socket (via a library like clamscan), or as a separate managed cloud service -- the uploaded file is scanned before being moved from a temporary quarantine location to permanent storage, and rejected (with the temporary file deleted) if the scan detects a threat, adding a crucial security layer for any application accepting file uploads from untrusted users.
const NodeClam = require('clamscan');
const clamscan = await new NodeClam().init({ clamdscan: { socket: '/var/run/clamav/clamd.ctl' } });
const { isInfected, viruses } = await clamscan.isInfected(req.file.path);
if (isInfected) {
await fs.promises.unlink(req.file.path);
return res.status(400).json({ error: 'File failed security scan', viruses });
}
Real-world example A document-sharing platform scans every uploaded file with ClamAV before moving it from a temporary quarantine folder to permanent cloud storage, ensuring a user can never inadvertently (or maliciously) distribute malware to other users through the platform's file-sharing feature.
Common follow-ups: How significant is the latency added by a virus scan to the overall upload flow, and how would you communicate that delay to the user?;What's the tradeoff of running this scan synchronously during the upload versus asynchronously after accepting the file into a quarantined state?
Security;Background Jobs & Queues
upload.single(fieldName) handles exactly one uploaded file under a specific form field name, populating req.file. upload.array(fieldName, maxCount) handles multiple files uploaded under the same field name (like several photos), populating req.files as an array. upload.fields([{ name, maxCount }, ...]) handles multiple distinct named fields, each potentially with its own file(s), useful when a form has several different types of file inputs (like a resume and a cover letter uploaded together).
app.post('/single', upload.single('avatar'), handler); // req.file
app.post('/multiple', upload.array('photos', 10), handler); // req.files (array)
app.post('/mixed', upload.fields([
{ name: 'resume', maxCount: 1 },
{ name: 'coverLetter', maxCount: 1 },
]), handler); // req.files.resume, req.files.coverLetter
Real-world example A job-application form uses upload.fields() to accept both a resume and a cover letter as two distinctly named file inputs in a single form submission, letting the handler access each one separately via req.files.resume and req.files.coverLetter.
Common follow-ups: What happens if a client attempts to upload more files than the maxCount limit specified for upload.array()?;How would you handle a form with a mix of both regular text fields and file uploads together?
Express & Middleware;HTTP & Web Servers
Chunked (resumable) uploads split a large file into smaller pieces on the client side, uploading each chunk as a separate request with metadata identifying its position within the overall file -- the server stores each received chunk (or appends it directly to a partially-assembled file) and, once all chunks have arrived, reassembles them into the complete file; if the connection drops mid-upload, only the remaining unsent chunks need to be retried, rather than restarting the entire upload from scratch.
app.post('/upload-chunk', upload.single('chunk'), async (req, res) => {
const { uploadId, chunkIndex, totalChunks } = req.body;
await fs.promises.rename(req.file.path, `./chunks/${uploadId}-${chunkIndex}`);
const receivedChunks = await fs.promises.readdir('./chunks');
if (receivedChunks.filter(f => f.startsWith(uploadId)).length === Number(totalChunks)) {
await assembleChunksIntoFinalFile(uploadId, totalChunks);
}
res.json({ received: chunkIndex });
});
Real-world example A cloud-storage application supporting multi-gigabyte file uploads over potentially unstable mobile network connections implements chunked uploads, letting a user's upload resume from where it left off after a dropped connection rather than needing to restart the entire multi-gigabyte transfer.
Common follow-ups: How does a library like tus (an open protocol for resumable uploads) standardize this pattern rather than each application implementing it from scratch?;How do you handle cleaning up abandoned partial chunk uploads that are never completed?
Streams & Buffers;Error Handling
Storing uploaded files on local disk ties that data to a specific server instance, which breaks horizontal scaling (a file uploaded to one instance wouldn't be visible from requests handled by a different instance) and risks permanent data loss if that specific instance is terminated (common in cloud environments with ephemeral, disposable instances) -- the standard solution is storing uploaded files in a dedicated object storage service like Amazon S3, which is durable, accessible from any application instance, and decoupled entirely from the application server's own lifecycle.
// Fragile: tied to this specific server's local disk
await fs.promises.writeFile(`./uploads/${filename}`, fileBuffer);
// Durable and scalable: stored in shared cloud object storage
await s3Client.send(new PutObjectCommand({ Bucket: 'uploads', Key: filename, Body: fileBuffer }));
Real-world example A photo-sharing app that originally stored uploads on local disk lost user photos whenever a server instance was replaced during a routine deployment, until migrating to S3-based storage, after which uploaded files persisted reliably regardless of which server instance handled the original upload or any later requests for it.
Common follow-ups: How does this relate to the twelve-factor app principle of keeping application instances stateless, discussed earlier in Cloud & DevOps?;What's an appropriate migration strategy for moving from local-disk storage to S3 for an application already in production with existing uploaded files?
Cloud & DevOps;Architecture & Design Patterns