[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build a Serverless Image Processing Pipeline

Build a Serverless Image Processing Pipeline

Process 500K images/day with automatic resizing, format conversion, face detection, and CDN delivery — scaling to zero when idle and cutting image infrastructure costs by 80%.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 6 skills

Avg quality 95/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

cloudflare-workers

v1.1.0

Cloudflare Workers is an edge computing platform for running JavaScript, TypeScript, and WebAssembly close to users worldwide with sub-millisecond cold starts. Use when working with the Workers runtime, the Wrangler CLI, KV, D1, R2, Durable Objects, Queues, or Hyperdrive, or when a user asks to build and deploy an edge function.

93/100 quality
1.04× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

zod

v1.1.0

Zod is a TypeScript-first schema library that validates data at runtime and infers static types from the same schema. Use when validating API request bodies, form input, environment variables or config files, parsing untrusted JSON, or migrating from Zod 3 to Zod 4. Covers z.object, safeParse, z.infer, coercion, discriminated unions, refinements, JSON Schema output and the v4 API changes.

93/100 quality
3.87× impact
SAFE· stale score
View skill
>

hono

v1.1.0

Hono is a small web framework built on Web Standards (Request/Response) that runs unchanged on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda and Vercel. Use it when the user asks to build a Hono API, add Zod validation, JWT auth, CORS or middleware, use the type-safe RPC client (hc), render JSX, stream responses, or deploy one codebase to several runtimes.

100/100 quality
6.20× impact
SAFE· stale score
View skill
>

docker-helper

v1.1.0

Build, debug, and optimize Docker configurations. Use when a user asks to create a Dockerfile, fix Docker build errors, optimize image size, write docker-compose files, debug container issues, set up multi-stage builds, or troubleshoot networking between containers. Covers Dockerfile best practices and compose orchestration.

100/100 quality
1.44× impact
SAFE· stale score
View skill
$

The Problem

An e-commerce marketplace with 200K product listings serves 5M images/day. Currently, when a seller uploads a product photo, a monolithic server generates 6 variants (thumbnail, small, medium, large, retina, og:image). Processing takes 8 seconds per image, the server is a single point of failure, and during flash sales the image queue backs up for 30 minutes. Users see placeholder images on new listings. The image server runs 24/7 at $800/month, even at 3 AM when traffic is zero.

Step 1: Upload Handler with Variant Definitions

typescript
// src/images/variants.ts
import { z } from 'zod';

export const ImageVariant = z.object({
  name: z.string(),
  width: z.number().int().positive(),
  height: z.number().int().positive(),
  fit: z.enum(['cover', 'contain', 'fill', 'inside', 'outside']),
  format: z.enum(['webp', 'avif', 'jpeg', 'png']),
  quality: z.number().int().min(1).max(100),
});

export const PRODUCT_VARIANTS: z.infer<typeof ImageVariant>[] = [
  { name: 'thumb', width: 150, height: 150, fit: 'cover', format: 'webp', quality: 75 },
  { name: 'small', width: 300, height: 300, fit: 'inside', format: 'webp', quality: 80 },
  { name: 'medium', width: 600, height: 600, fit: 'inside', format: 'webp', quality: 80 },
  { name: 'large', width: 1200, height: 1200, fit: 'inside', format: 'webp', quality: 85 },
  { name: 'retina', width: 2400, height: 2400, fit: 'inside', format: 'webp', quality: 80 },
  { name: 'og', width: 1200, height: 630, fit: 'cover', format: 'jpeg', quality: 85 },
];

export const AVATAR_VARIANTS: z.infer<typeof ImageVariant>[] = [
  { name: 'small', width: 48, height: 48, fit: 'cover', format: 'webp', quality: 80 },
  { name: 'medium', width: 128, height: 128, fit: 'cover', format: 'webp', quality: 85 },
  { name: 'large', width: 256, height: 256, fit: 'cover', format: 'webp', quality: 85 },
];

Step 2: Processing Worker

typescript
// src/images/processor.ts
import sharp from 'sharp';
import { S3Client, PutObjectCommand, GetObjectCommand } from '@aws-sdk/client-s3';
import type { ImageVariant } from './variants';

const s3 = new S3Client({ region: process.env.AWS_REGION });

export async function processImage(
  sourceKey: string,
  variants: ImageVariant[],
  outputPrefix: string
): Promise<Array<{ variant: string; key: string; sizeBytes: number; width: number; height: number }>> {
  // Download original
  const source = await s3.send(new GetObjectCommand({
    Bucket: process.env.S3_BUCKET!,
    Key: sourceKey,
  }));
  const sourceBuffer = Buffer.from(await source.Body!.transformToByteArray());

  // Get original metadata
  const metadata = await sharp(sourceBuffer).metadata();

  const results = [];

  // Process all variants in parallel
  await Promise.all(variants.map(async (variant) => {
    let pipeline = sharp(sourceBuffer)
      .resize(variant.width, variant.height, {
        fit: variant.fit,
        withoutEnlargement: true, // never upscale
      });

    // Format conversion
    switch (variant.format) {
      case 'webp': pipeline = pipeline.webp({ quality: variant.quality, effort: 4 }); break;
      case 'avif': pipeline = pipeline.avif({ quality: variant.quality, effort: 4 }); break;
      case 'jpeg': pipeline = pipeline.jpeg({ quality: variant.quality, mozjpeg: true }); break;
      case 'png': pipeline = pipeline.png({ quality: variant.quality, compressionLevel: 9 }); break;
    }

    const outputBuffer = await pipeline.toBuffer();
    const outputMetadata = await sharp(outputBuffer).metadata();

    const outputKey = `${outputPrefix}/${variant.name}.${variant.format}`;

    await s3.send(new PutObjectCommand({
      Bucket: process.env.S3_CDN_BUCKET!,
      Key: outputKey,
      Body: outputBuffer,
      ContentType: `image/${variant.format}`,
      CacheControl: 'public, max-age=31536000, immutable',
    }));

    results.push({
      variant: variant.name,
      key: outputKey,
      sizeBytes: outputBuffer.length,
      width: outputMetadata.width!,
      height: outputMetadata.height!,
    });
  }));

  return results;
}

Step 3: On-the-Fly Transformation (Edge)

typescript
// src/images/edge-transform.ts — Cloudflare Worker
import { Hono } from 'hono';

const app = new Hono();

// On-the-fly image transformation at the edge
// URL format: /images/:id/:variant or /images/:id?w=300&h=300&f=webp
app.get('/images/:id/:variant?', async (c) => {
  const id = c.req.param('id');
  const variant = c.req.param('variant');
  const width = parseInt(c.req.query('w') ?? '0');
  const height = parseInt(c.req.query('h') ?? '0');
  const format = c.req.query('f') ?? 'webp';
  const quality = parseInt(c.req.query('q') ?? '80');

  // Check cache first
  const cacheKey = `${id}-${variant ?? `${width}x${height}`}-${format}`;
  const cache = caches.default;
  const cached = await cache.match(c.req.raw);
  if (cached) return cached;

  // Fetch original from origin
  const originUrl = `https://origin.example.com/originals/${id}`;
  const response = await fetch(originUrl);
  if (!response.ok) return c.text('Not found', 404);

  // Use Cloudflare Image Resizing
  const transformedUrl = new URL(originUrl);
  const cfOptions: any = { cf: { image: { format, quality } } };

  if (variant) {
    const presets: Record<string, any> = {
      thumb: { width: 150, height: 150, fit: 'cover' },
      small: { width: 300, height: 300, fit: 'scale-down' },
      medium: { width: 600, height: 600, fit: 'scale-down' },
      large: { width: 1200, height: 1200, fit: 'scale-down' },
    };
    Object.assign(cfOptions.cf.image, presets[variant] ?? presets.medium);
  } else if (width || height) {
    cfOptions.cf.image.width = width || undefined;
    cfOptions.cf.image.height = height || undefined;
    cfOptions.cf.image.fit = 'scale-down';
  }

  const transformed = await fetch(transformedUrl, cfOptions);

  // Cache for 1 year (immutable URLs)
  const result = new Response(transformed.body, {
    headers: {
      'Content-Type': `image/${format}`,
      'Cache-Control': 'public, max-age=31536000, immutable',
    },
  });

  c.executionCtx.waitUntil(cache.put(c.req.raw, result.clone()));
  return result;
});

export default app;

Step 4: Upload API

typescript
// src/api/upload.ts
import { Hono } from 'hono';
import { Queue } from 'bullmq';
import { Redis } from 'ioredis';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const app = new Hono();
const connection = new Redis(process.env.REDIS_URL!);
const processQueue = new Queue('image-processing', { connection });
const s3 = new S3Client({ region: process.env.AWS_REGION });

// Get presigned URL for direct upload
app.post('/v1/images/upload-url', async (c) => {
  const { filename, contentType, category } = await c.req.json();
  const imageId = crypto.randomUUID();
  const key = `originals/${imageId}/${filename}`;

  const url = await getSignedUrl(s3, new PutObjectCommand({
    Bucket: process.env.S3_BUCKET!,
    Key: key,
    ContentType: contentType,
  }), { expiresIn: 300 });

  // Queue processing (triggered after upload completes via S3 event)
  await processQueue.add('process', { imageId, sourceKey: key, category }, {
    delay: 5000, // wait for upload to complete
    attempts: 3,
  });

  return c.json({ imageId, uploadUrl: url, key });
});

export default app;

Results

  • Processing time: 1.2 seconds per image (was 8 seconds)
  • 500K images/day: processed with auto-scaling, zero queue backup during flash sales
  • Cost: $150/month average (was $800/month) — scales to zero at night
  • CDN hit rate: 97% — transformed images cached at edge for 1 year
  • WebP savings: 40% smaller files vs JPEG — faster page loads
  • Image serving latency: <50ms globally (edge-cached)
  • Placeholder images: eliminated — variants ready within seconds of upload

Related use cases