Skip to content

Images API

The @cloudwerk/images package provides integration with Cloudflare’s image services: Hosted Images for storage and delivery, Image Transformer for CDN-style resizing, and the IMAGES binding for transform pipelines.

Terminal window
pnpm add @cloudwerk/images

Cloudflare offers three distinct image services, each suited for different use cases:

ProductPurposeRequirements
Hosted ImagesStore & serve images via CDNAccount ID, API token, paid plan
Image TransformerResize images via URL paramsZone-level route, paid plan
IMAGES BindingTransform pipeline in Workerwrangler.toml binding, paid plan

Creates an image definition with named variants.

import { defineImage } from '@cloudwerk/images'
export default defineImage({
variants: Record<string, ImageVariant>,
accountId?: string | EnvRef,
apiToken?: string | EnvRef,
})
ParameterTypeDescription
variantsRecord<string, ImageVariant>Named transformation variants
accountIdstring | EnvRefCloudflare account ID (default: { env: 'CF_ACCOUNT_ID' })
apiTokenstring | EnvRefCloudflare API token (default: { env: 'CF_IMAGES_TOKEN' })

Returns an ImageDefinition object that Cloudwerk registers automatically.

Transformation options for a variant.

interface ImageVariant {
width?: number // Width in pixels
height?: number // Height in pixels
fit?: 'cover' | 'contain' | 'scale-down' | 'crop' | 'pad'
format?: 'webp' | 'avif' | 'jpeg' | 'png' | 'auto'
quality?: number // 1-100
dpr?: number // Device pixel ratio (1-3)
gravity?: 'auto' | 'center' | 'top' | 'bottom' | 'left' | 'right' | 'face'
sharpen?: number // 0-10
blur?: number // 1-250
brightness?: number // -1 to 1
contrast?: number // -1 to 1
rotate?: 0 | 90 | 180 | 270
metadata?: 'keep' | 'copyright' | 'none'
}
// app/images/avatars.ts
import { defineImage } from '@cloudwerk/images'
export default defineImage({
variants: {
thumbnail: { width: 100, height: 100, fit: 'cover' },
profile: { width: 400, height: 400, fit: 'cover' },
large: { width: 800, fit: 'scale-down', quality: 90 },
},
})

Reference environment variables for credentials.

interface EnvRef {
env: string // Environment variable name
}
// Example
export default defineImage({
variants: { /* ... */ },
accountId: { env: 'CLOUDFLARE_ACCOUNT_ID' },
apiToken: { env: 'CLOUDFLARE_IMAGES_TOKEN' },
})

The image client provides methods for uploading, retrieving, and managing images.

Access image clients via the images proxy.

import { images } from '@cloudwerk/core/bindings'
// Upload an image
const result = await images.avatars.upload(file)
// Get variant URL
const url = images.avatars.url(result.id, 'thumbnail')

Upload an image and receive an image ID.

const result = await images.avatars.upload(file: File | Blob | ArrayBuffer, options?: UploadOptions)
interface UploadOptions {
id?: string // Custom image ID (auto-generated if not provided)
metadata?: Record<string, string> // Custom metadata
requireSignedUrls?: boolean // Require signed URLs
}
interface ImageResult {
id: string // Image ID
filename?: string // Original filename
metadata?: Record<string, string> // Custom metadata
uploaded: Date // Upload timestamp
variants: string[] // Available variant names
}

Get the URL for an image variant.

const url = images.avatars.url(id: string, variant: string): string
import { images } from '@cloudwerk/core/bindings'
import { json } from '@cloudwerk/core'
export async function POST(request: Request) {
const formData = await request.formData()
const file = formData.get('avatar') as File
const result = await images.avatars.upload(file, {
metadata: { userId: 'user-123' },
})
return json({
id: result.id,
thumbnail: images.avatars.url(result.id, 'thumbnail'),
profile: images.avatars.url(result.id, 'profile'),
})
}

Generate a URL for direct browser uploads.

const directUpload = await images.avatars.getDirectUploadUrl(options?: DirectUploadOptions)
interface DirectUploadOptions {
id?: string // Custom image ID
metadata?: Record<string, string> // Custom metadata
requireSignedUrls?: boolean // Require signed URLs
expiry?: Date // URL expiration
}
interface DirectUploadResult {
id: string // Image ID
uploadUrl: string // URL for browser upload
}

List uploaded images.

const images = await images.avatars.list(options?: ListOptions)
interface ListOptions {
page?: number // Page number (1-based)
perPage?: number // Results per page (max 1000)
}

Delete an image.

await images.avatars.delete(id: string)

Get image details.

const details = await images.avatars.get(id: string)

Creates a route handler for CDN-style image resizing using Cloudflare’s Image Resizing service.

import { createImageTransformer } from '@cloudwerk/images'
export const GET = createImageTransformer(config?: TransformerConfig)
interface TransformerConfig {
allowedOrigins?: string[] // Allowed image source origins
presets?: Record<string, TransformPreset> // Named presets
defaults?: TransformPreset // Default transformations
maxWidth?: number // Max width (default: 4096)
maxHeight?: number // Max height (default: 4096)
cacheControl?: string // Cache header (default: 1 year)
allowArbitrary?: boolean // Allow arbitrary params (default: true)
validateSource?: (url: URL) => boolean | Promise<boolean> // Custom validation
}
interface TransformPreset {
width?: number
height?: number
fit?: 'cover' | 'contain' | 'scale-down' | 'crop' | 'pad'
format?: 'webp' | 'avif' | 'jpeg' | 'png' | 'auto'
quality?: number // 1-100
dpr?: number // 1-3
gravity?: 'auto' | 'center' | 'top' | 'bottom' | 'left' | 'right' | 'face'
sharpen?: number // 0-10
blur?: number // 1-250
brightness?: number // -1 to 1
contrast?: number // -1 to 1
rotate?: 0 | 90 | 180 | 270
metadata?: 'keep' | 'copyright' | 'none'
background?: string // Hex or RGB for padding
}
// app/cdn/images/[...path]/route.ts
import { createImageTransformer } from '@cloudwerk/images'
export const GET = createImageTransformer({
allowedOrigins: ['https://images.mysite.com'],
presets: {
thumbnail: { width: 100, height: 100, fit: 'cover' },
hero: { width: 1920, height: 1080, fit: 'cover' },
},
defaults: {
format: 'auto',
quality: 85,
},
})

The transformer parses the source URL from the path and options from query params:

/cdn/images/https://images.mysite.com/photo.jpg?preset=thumbnail
/cdn/images/https://images.mysite.com/photo.jpg?w=800&h=600&fit=cover
ParameterAliasDescription
preset-Named preset from config
widthwWidth in pixels
heighthHeight in pixels
fit-Fit mode
formatfOutput format
qualityqQuality (1-100)
dpr-Device pixel ratio
gravity-Crop gravity
blur-Blur amount
sharpen-Sharpen amount
brightness-Brightness adjustment
contrast-Contrast adjustment
rotate-Rotation degrees

Convert an ImageVariant to a TransformPreset for reusing hosted image variants.

import { variantToPreset, createImageTransformer } from '@cloudwerk/images'
import avatars from '../images/avatars'
const presets = Object.fromEntries(
Object.entries(avatars.variants).map(([name, variant]) => [
name,
variantToPreset(variant),
])
)
export const GET = createImageTransformer({ presets })

The IMAGES binding provides in-Worker image transformation without uploading to Cloudflare Images.

Add the binding to wrangler.toml:

[[images]]
binding = "MY_IMAGES"
interface CloudflareImagesBinding {
input(source: Blob | ArrayBuffer | ReadableStream<Uint8Array>): CloudflareImagesTransformBuilder
info(source: Blob | ArrayBuffer | ReadableStream<Uint8Array>): Promise<CloudflareImageInfo>
}
interface CloudflareImageInfo {
width: number
height: number
format: string
fileSize: number
}

Fluent API for chaining transformations.

interface CloudflareImagesTransformBuilder {
transform(options: CloudflareImageTransformOptions): CloudflareImagesTransformBuilder
output(options: CloudflareImageOutputOptions): CloudflareImagesTransformBuilder
response(): Promise<Response>
blob(): Promise<Blob>
arrayBuffer(): Promise<ArrayBuffer>
stream(): ReadableStream<Uint8Array>
}
import { getBinding } from '@cloudwerk/core/bindings'
import type { CloudflareImagesBinding } from '@cloudwerk/images'
export async function POST(request: Request) {
const IMAGES = getBinding<CloudflareImagesBinding>('MY_IMAGES')
const formData = await request.formData()
const file = formData.get('image') as File
// Transform the image
const response = await IMAGES
.input(await file.arrayBuffer())
.transform({ width: 800, rotate: 90 })
.output({ format: 'image/webp', quality: 85 })
.response()
return response
}
interface CloudflareImageTransformOptions {
width?: number
height?: number
fit?: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad'
gravity?: 'auto' | 'left' | 'right' | 'top' | 'bottom' | 'center' | 'face'
quality?: number // 1-100
dpr?: number // 1-3
rotate?: 0 | 90 | 180 | 270
sharpen?: number // 0-10
blur?: number // 1-250
brightness?: number // -1 to 1
contrast?: number // -1 to 1
background?: string // Hex or RGB
border?: { color: string; width: number }
trim?: { top?: number; right?: number; bottom?: number; left?: number }
}
interface CloudflareImageOutputOptions {
format?: 'image/jpeg' | 'image/png' | 'image/webp' | 'image/avif' | 'image/gif'
quality?: number // 1-100
metadata?: 'keep' | 'copyright' | 'none'
}

Built-in presets for common use cases.

import { IMAGE_PRESETS } from '@cloudwerk/images'
// Available presets
IMAGE_PRESETS.thumbnail // 100x100 cover
IMAGE_PRESETS.preview // 400x400 contain
IMAGE_PRESETS.large // 1200px wide
IMAGE_PRESETS.hero // 1920x1080 cover
IMAGE_PRESETS.social // 1200x630 (Open Graph)
IMAGE_PRESETS.mobile // 640px wide, WebP

Base error class for image operations.

import { ImageError } from '@cloudwerk/images'
class ImageError extends Error {
readonly code: string
}

Invalid image configuration.

class ImageConfigError extends ImageError {
readonly code: 'IMAGE_CONFIG_ERROR'
}

Image upload failed.

class ImageUploadError extends ImageError {
readonly code: 'IMAGE_UPLOAD_ERROR'
readonly status?: number
}

Image not found.

class ImageNotFoundError extends ImageError {
readonly code: 'IMAGE_NOT_FOUND'
readonly imageId: string
}

Invalid variant name.

class ImageVariantError extends ImageError {
readonly code: 'IMAGE_VARIANT_ERROR'
readonly variantName: string
readonly availableVariants: string[]
}

Image transformation failed.

class ImageTransformError extends ImageError {
readonly code: string
readonly status: number
}

Get a typed image client by name.

import { getImages } from '@cloudwerk/core/bindings'
const avatars = getImages('avatars')
await avatars.upload(file)

Check if an image definition exists.

import { hasImages } from '@cloudwerk/core/bindings'
if (hasImages('avatars')) {
await images.avatars.upload(file)
}

List all available image definition names.

import { getImagesNames } from '@cloudwerk/core/bindings'
const available = getImagesNames()
// ['avatars', 'products', 'banners']

interface ImageDefinition<V extends Record<string, ImageVariant>> {
variants: V
accountId?: string | EnvRef
apiToken?: string | EnvRef
}
interface ImageClientInterface<V extends string = string> {
upload(source: File | Blob | ArrayBuffer, options?: UploadOptions): Promise<ImageResult>
url(id: string, variant: V): string
get(id: string): Promise<ImageResult>
delete(id: string): Promise<void>
list(options?: ListOptions): Promise<ImageResult[]>
getDirectUploadUrl(options?: DirectUploadOptions): Promise<DirectUploadResult>
}