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,
};
}
}
import { Request, Response } from 'express';
import { promises as fs } from 'fs';
import path from 'path';
import { randomUUID } from 'crypto';
import { ThumbnailService } from './ThumbnailService';
const SUPPORTED = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/avif']);
const OUTPUT_DIR = path.join(process.cwd(), 'storage', 'thumbnails');
export class UploadController {
constructor(private readonly thumbnails = new ThumbnailService()) {}
private isSupported(mime: string): boolean {
return SUPPORTED.has(mime);
}
handleUpload = async (req: Request, res: Response): Promise<Response> => {
const file = req.file;
if (!file) {
return res.status(400).json({ error: 'No file uploaded' });
}
if (!this.isSupported(file.mimetype)) {
return res.status(415).json({ error: `Unsupported type: ${file.mimetype}` });
}
try {
const result = await this.thumbnails.generate(file.buffer);
const id = randomUUID();
await fs.mkdir(OUTPUT_DIR, { recursive: true });
const written = await Promise.all(
result.variants.map(async (v) => {
const filename = `${id}-${v.name}.webp`;
await fs.writeFile(path.join(OUTPUT_DIR, filename), v.buffer);
return { name: v.name, width: v.width, height: v.height, url: `/thumbnails/${filename}` };
})
);
return res.status(201).json({ id, original: result.original, variants: written });
} catch (err) {
const message = err instanceof Error ? err.message : 'processing failed';
return res.status(422).json({ error: `Could not process image: ${message}` });
}
};
}
import { Router } from 'express';
import multer from 'multer';
import { UploadController } from './UploadController';
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: 10 * 1024 * 1024, // 10MB
files: 1,
},
});
const controller = new UploadController();
const router = Router();
router.post('/uploads', upload.single('image'), controller.handleUpload);
export default router;
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
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
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)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
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
# Installation
# rails active_storage:install
# rails db:migrate
# config/storage.yml
local:
ActiveStorage for file uploads and attachments
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)
Share this code
Here's the card — post it anywhere.