Image Upload & Thumbnails Tutorial · Module 02 of 11

File Upload & Validation

Implement POST /upload endpoint with file validation: check MIME type, enforce size limits, scan for malware. Store original images to disk or S3. Reject invalid files gracefully. By the end, users can safely upload images.

~3–4 hrsIntermediateUpload focus
← Back to Module 02 overview
What You'll Have at the End

Definition of Done

  • POST /upload accepts multipart form data with image file.
  • MIME type validation: accept only image/jpeg, image/png, image/webp.
  • File size validation: reject files > 50MB.
  • Malware scanning: integrate ClamAV or similar (or use basic magic bytes check).
  • Store original to disk (./uploads/originals/) or S3.
  • Create database record: images table with metadata.
  • Return image ID and metadata in response.
  • Proper HTTP status codes: 400 for validation errors, 413 for too large, 413 for malware.
The Steps

Build It

STEP 1

Implement file validation middleware

Create src/middleware/fileValidator.ts:

import { Request, Response, NextFunction } from 'express';

const ALLOWED_MIMES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB

export function validateImageFile(
  req: Request,
  res: Response,
  next: NextFunction
) {
  if (!req.file) {
    return res.status(400).json({ error: 'No file provided' });
  }

  if (!ALLOWED_MIMES.includes(req.file.mimetype)) {
    return res.status(400).json({
      error: 'Invalid file type. Allowed: JPEG, PNG, WebP'
    });
  }

  if (req.file.size > MAX_FILE_SIZE) {
    return res.status(413).json({
      error: `File too large. Max ${MAX_FILE_SIZE / 1024 / 1024}MB`
    });
  }

  // Basic magic bytes check for JPG and PNG
  const buffer = req.file.buffer;
  const isValidJpg = buffer[0] === 0xff && buffer[1] === 0xd8;
  const isValidPng = buffer[0] === 0x89 && buffer[1] === 0x50;
  const isValidWebp = buffer.toString('utf-8', 0, 4) === 'RIFF';

  if (!isValidJpg && !isValidPng && !isValidWebp) {
    return res.status(400).json({ error: 'File signature does not match type' });
  }

  next();
}