Image Upload & Thumbnails Tutorial · Module 06 of 11

Image Transformations & Filters

Apply on-the-fly transformations: resize, crop, rotate, blur, grayscale, sepia. Support query parameters for dynamic transformations. Cache transformed results for performance. By the end, clients can request any image with any transformation without pre-generating every variant.

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

Definition of Done

  • Query parameters for transformations: ?width=400&height=300&format=webp&quality=80.
  • Filters: ?blur=10&grayscale=1&sepia=1&rotate=90.
  • Validate parameters: width/height max 4000px, quality 0-100.
  • Cache transformed images in Redis (1-day TTL).
  • GET /images/:id/transform?...params returns transformed image.
  • Return appropriate Content-Type and Cache-Control headers.
  • Performance: transform + serve in < 500ms (cache hit < 50ms).
The Steps

Build It

STEP 1

Implement image transformation pipeline

Create src/utils/imageTransform.ts:

import sharp, { Sharp } from 'sharp';

export interface TransformOptions {
  width?: number;
  height?: number;
  format?: 'jpeg' | 'png' | 'webp' | 'avif';
  quality?: number;
  blur?: number;
  grayscale?: boolean;
  sepia?: boolean;
  rotate?: number;
}

export async function transformImage(
  buffer: Buffer,
  options: TransformOptions
): Promise {
  let image = sharp(buffer);

  // Validate and constrain dimensions
  const maxDim = 4000;
  const width = options.width ? Math.min(options.width, maxDim) : undefined;
  const height = options.height ? Math.min(options.height, maxDim) : undefined;

  if (width || height) {
    image = image.resize(width, height, { fit: 'inside', withoutEnlargement: true });
  }

  if (options.rotate) {
    image = image.rotate(options.rotate);
  }

  if (options.blur) {
    image = image.blur(Math.min(options.blur, 50));
  }

  if (options.grayscale) {
    image = image.grayscale();
  }

  if (options.sepia) {
    image = image.tint({ r: 112, g: 66, b: 20 });
  }

  const format = options.format || 'jpeg';
  const quality = Math.min(Math.max(options.quality || 80, 0), 100);

  return image.toFormat(format, { quality }).toBuffer();
}