typescript 119 lines · 3 tabs

Generate WebP Image Thumbnails on Upload with Sharp and Express

Shared by codesnips Aug 2026
3 tabs
import sharp from 'sharp';

export interface Variant {
  name: string;
  width: number;
  quality: number;
}

export interface GeneratedVariant {
  name: string;
  width: number;
  height: number;
  buffer: Buffer;
}

export interface ThumbnailResult {
  original: { width: number; height: number };
  variants: GeneratedVariant[];
}

export class ThumbnailService {
  static readonly VARIANTS: Variant[] = [
    { name: 'thumb', width: 150, quality: 70 },
    { name: 'small', width: 480, quality: 78 },
    { name: 'medium', width: 1024, quality: 82 },
  ];

  async generate(source: Buffer): Promise<ThumbnailResult> {
    const meta = await sharp(source).metadata();

    const variants = await Promise.all(
      ThumbnailService.VARIANTS.map(async (variant) => {
        const { data, info } = await sharp(source)
          .rotate() // respect EXIF orientation
          .resize(variant.width, undefined, {
            fit: 'inside',
            withoutEnlargement: true,
          })
          .webp({ quality: variant.quality, effort: 4 })
          .toBuffer({ resolveWithObject: true });

        return {
          name: variant.name,
          width: info.width,
          height: info.height,
          buffer: data,
        };
      })
    );

    return {
      original: { width: meta.width ?? 0, height: meta.height ?? 0 },
      variants,
    };
  }
}
3 files · typescript Explain with highlit

This snippet shows the full path an uploaded image takes through an Express service: from the raw multipart buffer to a set of compressed, resized derivatives written to disk. The work is split so that the HTTP concern (parsing, validating, responding) stays in the controller while the CPU-bound pixel work lives in a reusable service backed by sharp, the de-facto libvips binding for Node.

In ThumbnailService, the core idea is a declared set of VARIANTS — a small table of named sizes with target width and quality. generate runs each variant through a fresh sharp pipeline rather than reusing one instance, because a sharp object is single-use once a format encoder is attached. Each pipeline calls resize with fit: 'inside' and withoutEnlargement: true, which preserves aspect ratio and refuses to upscale small originals — a common pitfall where a tiny avatar gets blown up into a blurry thumbnail. Output is forced to .webp() with a per-variant quality, giving strong compression versus the source JPEG/PNG. The variants are produced concurrently with Promise.all, and metadata() reads the source once so callers get the original dimensions back.

The withoutEnlargement flag is the reason resized.width may be smaller than the requested width, so the service reports the actual dimensions from each output rather than the requested ones. Failing the whole batch if any variant throws is intentional: a partial thumbnail set is worse than none.

In UploadController, handleUpload is the Express handler wired behind multer's memoryStorage, so req.file.buffer holds the bytes without a temp file. It validates presence and MIME type up front, delegates to the service, then persists results. isSupported guards against non-image uploads before any pixel work happens, which matters because sharp will happily spend CPU before erroring on garbage input.

In uploadRoutes, the route composes multer limits (a 10MB cap and single-file field) with the controller, keeping size enforcement at the edge. The trade-off of memoryStorage is that very large uploads live in RAM; for high-throughput services a disk or streaming strategy would be swapped in behind the same controller. This layering means the compression logic is unit-testable in isolation and reusable from a queue worker without touching Express.


Related snips

ruby
class SignupForm
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :account_name, :string
  attribute :email, :string

Shallow Controller, Deep Params: Form Object Pattern

rails activemodel form-object
by codesnips 3 tabs
typescript
import { randomBytes, createHash } from "crypto";
import jwt from "jsonwebtoken";
import { RefreshTokenStore } from "./store";

const ACCESS_SECRET = process.env.ACCESS_SECRET!;
const ACCESS_TTL = "15m";

JWT access + refresh token rotation (conceptual)

security node jwt
by codesnips 3 tabs
html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Form Validation Example</title>
  <style>

HTML forms with validation and accessibility

html forms validation
by Alex Chang 1 tab
python
import cv2

image = cv2.imread('receipt.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
thresholded = cv2.adaptiveThreshold(

OpenCV image preprocessing for OCR and vision pipelines

opencv image-processing computer-vision
by Dr. Elena Vasquez 1 tab
ruby
# Installation
# rails active_storage:install
# rails db:migrate

# config/storage.yml
local:

ActiveStorage for file uploads and attachments

ruby rails active-storage
by Sarah Mitchell 2 tabs
typescript
import type { IncomingMessage, ServerResponse } from "http";

const MIN_BYTES = 1024;

const INCOMPRESSIBLE = /^(image|video|audio)\/|application\/(zip|gzip|x-brotli|pdf|octet-stream)/i;

Response compression (only when it helps)

performance http express
by codesnips 3 tabs

Share this code

Here's the card — post it anywhere.

Generate WebP Image Thumbnails on Upload with Sharp and Express — share card
Link copied