🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

How to Handle File Uploads in a Node.js API: Complete 2026 Guide

⏱️5 min read  ·  969 words

Handling file uploads correctly involves more than just accepting a file — you need validation, storage strategy, security, and often image processing. This guide covers robust file uploads in Node.js from simple local storage to scalable cloud uploads.

Upload Strategies Overview

Strategy Best For
Local disk storage Small apps, single server
Upload to your server, then cloud Processing needed before storage
Direct-to-cloud (presigned URLs) Large files, scale, best practice

Basic Upload with Multer

npm install multer
const multer = require('multer');
const path = require('path');

const storage = multer.diskStorage({
  destination: './uploads',
  filename: (req, file, cb) => {
    // Generate a safe, unique filename - never trust the client's name
    const unique = Date.now() + '-' + Math.round(Math.random() * 1e9);
    cb(null, unique + path.extname(file.originalname));
  },
});

const upload = multer({
  storage,
  limits: { fileSize: 5 * 1024 * 1024 },   // 5MB limit
  fileFilter: (req, file, cb) => {
    // Only allow specific types
    const allowed = ['image/jpeg', 'image/png', 'image/webp'];
    if (allowed.includes(file.mimetype)) {
      cb(null, true);
    } else {
      cb(new Error('Only JPEG, PNG, and WebP images allowed'));
    }
  },
});

app.post('/upload', upload.single('file'), (req, res) => {
  res.json({ filename: req.file.filename, size: req.file.size });
});

Validate Files Properly (Don’t Trust MIME Type Alone)

npm install file-type
const { fileTypeFromBuffer } = require('file-type');
const fs = require('fs').promises;

// The client-provided MIME type can be faked - verify actual content
async function validateFile(filePath) {
  const buffer = await fs.readFile(filePath);
  const type = await fileTypeFromBuffer(buffer);

  const allowed = ['image/jpeg', 'image/png', 'image/webp'];
  if (!type || !allowed.includes(type.mime)) {
    await fs.unlink(filePath);   // delete the invalid file
    throw new Error('Invalid file content');
  }
  return type;
}

Multiple Files and Fields

// Multiple files under one field
app.post('/gallery', upload.array('photos', 10), (req, res) => {
  const files = req.files.map(f => ({ filename: f.filename, size: f.size }));
  res.json({ uploaded: files });
});

// Different fields
const uploadFields = upload.fields([
  { name: 'avatar', maxCount: 1 },
  { name: 'documents', maxCount: 5 },
]);
app.post('/profile', uploadFields, (req, res) => {
  const avatar = req.files.avatar?.[0];
  const docs = req.files.documents || [];
  res.json({ avatar: avatar?.filename, docCount: docs.length });
});

Image Processing with Sharp

npm install sharp
const sharp = require('sharp');

// Resize and optimize uploaded images
async function processImage(inputPath, outputPath) {
  await sharp(inputPath)
    .resize(1200, 1200, { fit: 'inside', withoutEnlargement: true })
    .webp({ quality: 80 })       // convert to WebP, compress
    .toFile(outputPath);
}

// Generate a thumbnail
async function makeThumbnail(inputPath, thumbPath) {
  await sharp(inputPath)
    .resize(200, 200, { fit: 'cover' })
    .toFile(thumbPath);
}

Direct-to-Cloud Upload with Presigned URLs (Best for Scale)

For large files and scale, don’t route uploads through your server — let clients upload directly to S3 using a presigned URL your backend generates:

const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');

const s3 = new S3Client({ region: 'us-east-1' });

// Backend: generate a presigned upload URL
app.post('/upload-url', async (req, res) => {
  const { filename, contentType } = req.body;
  const key = `uploads/${Date.now()}-${filename}`;

  const command = new PutObjectCommand({
    Bucket: 'my-bucket',
    Key: key,
    ContentType: contentType,
  });
  const uploadUrl = await getSignedUrl(s3, command, { expiresIn: 300 });

  res.json({ uploadUrl, key });
});
// Frontend: upload directly to S3, bypassing your server
const { uploadUrl, key } = await fetch('/upload-url', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ filename: file.name, contentType: file.type }),
}).then(r => r.json());

await fetch(uploadUrl, {
  method: 'PUT',
  headers: { 'Content-Type': file.type },
  body: file,   // uploads straight to S3 - no server bottleneck
});

Security Checklist

  • Validate file type by content, not just the client’s MIME type (which can be faked)
  • Limit file size at every layer (multer, nginx, cloud)
  • Generate safe filenames — never use the client’s filename directly (path traversal risk)
  • Store outside the web root or in cloud storage, not in a publicly-servable directory that could execute uploads
  • Scan for malware if accepting files from untrusted users
  • Set proper Content-Type and Content-Disposition when serving files back

Frequently Asked Questions

Q: Should uploads go through my server or directly to cloud storage?
A: For small files or when you need to process them first, through your server (multer). For large files and scale, use presigned URLs so clients upload directly to S3 — this bypasses your server’s bandwidth and size limits and scales far better.

Q: Why shouldn’t I trust the client’s MIME type?
A: The Content-Type header is client-controlled and easily faked — an attacker could upload a malicious file labeled as an image. Verify the actual file content with a library like file-type, which reads the file’s magic bytes.

Q: How do I handle very large file uploads?
A: Use presigned URLs for direct-to-cloud upload (no server bottleneck), or implement chunked/resumable uploads for huge files. Increase size limits at all layers (multer, nginx, cloud) if routing through your server, but direct-to-cloud is better for large files.

Q: Where should I store uploaded files?
A: Cloud object storage (S3, GCS, R2) for scalability and reliability — not on your app server’s disk (which doesn’t scale across instances and is lost if the server is replaced). For small single-server apps, local disk works but plan to migrate to cloud storage as you grow.

Q: How do I prevent uploaded files from being executed?
A: Store uploads outside the web root or in cloud storage, generate random filenames, validate content type, and serve files with proper headers (Content-Disposition: attachment for downloads). Never store user uploads in a directory where the web server would execute them as scripts.

Conclusion

Handling file uploads in Node.js requires more than accepting a file. Use multer for server-side uploads with size limits and type filtering, validate files by actual content (not the client’s MIME type), and process images with Sharp. For scale and large files, use presigned URLs to let clients upload directly to cloud storage, bypassing your server. Always follow security practices: validate content, limit sizes, generate safe filenames, and store files in cloud storage or outside the web root. Getting uploads right means balancing convenience, performance, and security — direct-to-cloud with proper validation is the modern best practice for most applications.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *