Orange Book
End-to-end projects

Project six, image converter

Use R2 for originals, Cloudflare Images for supported transformations, and explicit parameter, source, cost, and cache boundaries.

Edited and verified by Orange Book Editorial Team ·

PROJECT 06INTERMEDIATEabout 50 minutesOutcome: a verified upload and transform flow

Completion criteria

A supported upload receives a stable object key; transformation accepts only approved sizes and formats; the Worker cannot become an arbitrary URL proxy; repeated transforms can be cached; oversized, non-image, missing-source, and transform-failure cases are explicit.

Prerequisites and companion example

Unit tests need no Cloudflare account:

node --test examples/image-transformer/test/index.test.mjs

Six tests should pass, covering file signatures, server-generated object keys, preset mapping, arbitrary-URL rejection, loop protection, and downstream 404. A real transform additionally needs an R2 bucket, an HTTPS original-image origin you control, enabled Images Transformations, and that same host in allowed origins. Replace SOURCE_ORIGIN in wrangler.jsonc, then start:

cp examples/image-transformer/.dev.vars.example examples/image-transformer/.dev.vars
pnpm wrangler dev --config examples/image-transformer/wrangler.jsonc

Local preview and billing boundary

wrangler dev provides only a low-fidelity transform mock. Real Images transformations require account configuration, and each unique source/parameter combination can create billed usage. Do not aim automated tests at real transformations in bulk.

Two image paths

Cloudflare Images transformations can optimize remote images through a formatted URL or the cf.image options on a Worker fetch() and cache the transformed result. Use an Images binding when working with image bytes directly. R2 stores originals or generated objects that you own. These are separate responsibilities.

TaskSuggested boundary
URL resize, crop, quality/format optimizationImages transformations
Read or write an original objectR2 binding
Validate user parameters and authorizationWorker
Large non-interactive batchQueue plus background processor
Unsupported native codecEvaluate a Container or external processor

Safe transformation route

Never send an arbitrary user-provided url directly to fetch(). That can turn the utility into SSRF or a traffic proxy. Construct the source from a controlled object key and allowed origin, then accept only preset widths and fit behavior.

const allowedWidths = new Set([320, 640, 1280]);
const width = Number(new URL(request.url).searchParams.get('width') ?? 640);

if (!allowedWidths.has(width)) {
  return Response.json({ error: 'invalid_width' }, { status: 400 });
}

const source = new URL(`/uploads/${safeObjectKey}`, 'https://media.example.com');
return fetch(source, {
  cf: { image: { width, fit: 'scale-down' } },
});

safeObjectKey comes from authentication, ownership, and path normalization. Replace the example domain with a controlled source configured for transformations. Confirm cf.image parameters against current primary documentation and local types.

Upload and transform flow

Accept an upload intent

Verify user, plan, declared file size, and allowed MIME first. Generate a server-owned object key and short-lived upload contract so a user cannot choose bucket paths or overwrite another user's object.

Verify the real object

After upload, read object metadata and check size, media type, and ownership. A public list returns only visible objects for the current tenant. An internal key is not authorization.

Apply transformation presets

Map thumb, card, and hero to server-owned dimensions and fit values instead of arbitrary combinations. Include a stable preset/version in output URLs for caching and future changes.

Handle failures

Return 404 for a missing source, 400 for invalid parameters, and stable authentication/authorization states. A downstream transform failure returns a retryable error without internal URLs or stack traces.

Observe cost and abuse

Track uploaded bytes, objects, and transform requests by user or tenant. Transformations are billed to the account that owns the Worker, so a public transform route needs per-path rate controls and application quotas.

Reading experience

Before and after

Show the same image with real dimensions, format, and file size.

Preset cards

Users choose a purpose instead of understanding every low-level parameter.

Job state

Batch work shows queued, running, done, and failed with retry for failed items.

Accessibility

Keep alt text editable, use empty alt for decoration, and never auto-publish an unreviewed AI description.

Verification and troubleshooting

SymptomCheck firstRecovery
Upload returns 415MIME matches the real file signatureRe-export JPEG/PNG/WebP/AVIF; do not rename an extension
Transform returns invalid_source_originSOURCE_ORIGIN is HTTPS with no path, credentials, or queryFix server configuration and restart the Worker
Transform returns source_not_foundThe controlled origin serves the returned object keyCheck the R2 custom domain and object state
Response is image_transform_loopThe transform route also intercepts originalsSeparate /images/ from the original origin and keep Via protection

Remote boundary and rollback

Enabling Images transformations, creating an R2 bucket, configuring a custom domain, and deploying rate rules change remote state. Code rollback cannot restore an overwritten or deleted original. Use immutable versioned object keys and delay physical deletion. If a new preset fails, restore the route mapping while preserving previous preset URLs so published pages do not lose images.

Next: establish the website and application defense baseline.

Primary sources

Did this page help you complete your goal?

Beta feedback is generated in this browser and is never uploaded automatically.

On this page