infra-platform-cloudflare-workers
Cloudflare Workers edge compute platform — Wrangler CLI, KV, D1, R2, Durable Objects, Queues, Workers AI
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/infra-platform-cloudflare-workers/skills/infra-platform-cloudflare-workers
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Cloudflare Workers Patterns
Quick Guide: Cloudflare Workers run TypeScript/JavaScript on Cloudflare's global edge network with V8 isolates (not containers). Use
wrangler.jsoncfor configuration,wrangler devfor local development, andwrangler deployfor production. Access KV, D1, R2, Queues, Durable Objects, and Workers AI through type-safe bindings on theenvparameter. Runwrangler typesto auto-generate yourEnvinterface. Stream large payloads — Workers have a 128 MB memory limit. Never store request-scoped state in module-level variables.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST run wrangler types to generate your Env interface — NEVER hand-write binding types)
(You MUST use wrangler.jsonc for new projects — Cloudflare recommends JSON config and some features are JSON-only)
(You MUST stream large request/response bodies — NEVER buffer entire payloads in memory (128 MB limit))
(You MUST avoid module-level mutable state — Workers reuse V8 isolates across requests, causing cross-request data leaks)
(You MUST use bindings for Cloudflare services (KV, D1, R2, Queues) — NEVER use REST APIs from within Workers)
</critical_requirements>
Examples
- Core Setup & Configuration — wrangler.jsonc, project init, fetch handler, secrets, multi-env, CI/CD, testing
- KV Storage — KV binding, typed get/put, TTL, stale-while-revalidate caching
- D1 Database — D1 binding, parameterized queries, batch ops, migrations, CRUD API
- R2 Object Storage — R2 binding, file upload/download/delete with streaming
- Durable Objects — DO classes, SQLite, RPC, rate limiter, WebSocket chat
- Routing & Middleware — API framework integration, middleware, queues, cron, service bindings, AI, streaming
- Quick Reference — Wrangler CLI commands, binding type signatures, config template, CPU limits
Auto-detection: Cloudflare Workers, wrangler, wrangler.toml, wrangler.jsonc, Workers KV, Cloudflare KV, D1 database, R2 bucket, Durable Objects, Cloudflare Queues, Workers AI, service binding, miniflare, compatibility_date, compatibility_flags, nodejs_compat, cloudflare:workers, ExportedHandler, DurableObject, wrangler dev, wrangler deploy, wrangler types, Cloudflare Pages Functions, edge worker, CF Worker
When to use:
- Deploying TypeScript/JavaScript to Cloudflare's edge network
- Configuring Wrangler CLI for local development and deployment
- Using Cloudflare bindings: KV, D1, R2, Queues, Durable Objects, Workers AI
- Building APIs on Workers with a framework (e.g., Hono)
- Implementing real-time features with Durable Objects and WebSockets
- Setting up cron triggers and scheduled handlers
- Configuring service bindings for worker-to-worker communication
- Managing environment variables, secrets, and multi-environment deploys
When NOT to use:
- Long-running compute tasks exceeding CPU time limits (use traditional servers or Workflows)
- Applications requiring persistent TCP connections to external databases without Hyperdrive
- Workloads needing more than 128 MB memory per request
Key patterns covered:
- Wrangler configuration (
wrangler.jsonc) and project setup - Fetch handler, scheduled handler, and queue handler
- KV key-value storage (caching, config, session data)
- D1 SQLite database (relational data at the edge)
- R2 S3-compatible object storage (files, uploads, assets)
- Durable Objects (stateful edge compute, WebSockets, coordination)
- Cloudflare Queues (async message processing)
- Workers AI (inference at the edge)
- Framework integration (Hono examples) with typed bindings
- Environment variables, secrets, and multi-environment config
- Service bindings (worker-to-worker RPC)
- Cron triggers and scheduled workers
- Streaming and performance optimization
- Testing with Workers-native test pool
<decision_framework>
Decision Framework
Choosing a Storage Primitive
What kind of data?
|
+-- Key-value pairs (cache, config, sessions)
| +-- Read-heavy, eventual consistency OK --> KV
| +-- Strong consistency needed --> Durable Objects
|
+-- Relational data with queries
| +-- Edge-native SQLite --> D1
| +-- External PostgreSQL/MySQL --> Hyperdrive
|
+-- Files and blobs (images, documents)
| +-- S3-compatible storage --> R2
|
+-- Coordination state (chat, multiplayer, collaboration)
| +-- Single-threaded consistency --> Durable Objects
|
+-- Message passing / background work
+-- Simple fan-out, buffering --> Queues
+-- Multi-step durable execution --> Workflows
Choosing Between Workers and Pages
What are you building?
|
+-- API only (no frontend) --> Workers
|
+-- Static site + API --> Pages with Functions
|
+-- Full-stack with SSR --> Pages (framework) or Workers + Static Assets
|
+-- Background processing / cron --> Workers (Pages lacks cron support)
|
+-- Real-time / WebSockets --> Workers + Durable Objects
When to Use Durable Objects vs D1
Do you need coordination between concurrent requests?
|
+-- YES (chat, game, collaboration) --> Durable Objects
| +-- Single-threaded, no race conditions
| +-- WebSocket support with hibernation
| +-- Per-entity sharding (one DO per room/session)
|
+-- NO (CRUD, reporting, querying) --> D1
+-- Full SQL support
+-- Cross-entity queries
+-- Traditional database patterns
</decision_framework>
<red_flags>
RED FLAGS
High Priority Issues:
- Storing request-scoped data in module-level variables (V8 isolate reuse causes cross-request leaks)
- Buffering entire request/response bodies with
.text()/.arrayBuffer()(128 MB memory limit) - Hand-writing
Envinterface instead of runningwrangler types(mismatches between config and code) - Using REST APIs for KV/D1/R2/Queues from within Workers instead of bindings (unnecessary latency and auth overhead)
- Putting secrets in
wrangler.jsonc, source code, or environment variables (usewrangler secret put) - Creating a single global Durable Object for all traffic (bottleneck at ~1000 req/sec)
Medium Priority Issues:
- Using
wrangler.tomlfor new projects (JSON format recommended, some features JSON-only) - Outdated
compatibility_date(misses runtime improvements and bug fixes) - Missing
observabilityconfig (production Workers are a black box without logs/traces) - Destructuring
ctxin fetch handler (losesthisbinding,ctx.waitUntilthrows "Illegal invocation") - Using
exec()for D1 queries instead ofprepare().bind()(no parameterization, SQL injection risk) - Not reciprocating WebSocket close in Durable Objects (causes 1006 errors)
Common Mistakes:
- Forgetting that KV is eventually consistent (~60s propagation) and expecting instant reads after writes
- Using
Math.random()for security-sensitive tokens (usecrypto.randomUUID()orcrypto.getRandomValues()) - Not using
ctx.waitUntil()for post-response background work (work may be cancelled when response is sent) - Comparing secrets with
===instead ofcrypto.subtle.timingSafeEqual()(timing side-channel attack) - Using
passThroughOnException()as error handling (hides bugs, use explicit try/catch) - Floating promises (not awaited, not returned, not passed to
waitUntil()) causing silent failures — enable@typescript-eslint/no-floating-promisesto catch at dev time
Gotchas and Edge Cases:
- Bindings (KV, D1, R2, etc.) are NOT inherited across Wrangler environments — you must re-declare them per environment
wrangler devuses local simulation by default; use--remoteto test against real Cloudflare services- D1 batch operations execute sequentially (not in parallel) but atomically
- Durable Objects in-memory state is lost on eviction — always persist important data to SQLite first
- Unnecessary
awaitbetween DO storage writes breaks write coalescing — batch writes happen atomically when you don't await between them - DO alarm handlers may fire multiple times — design them to be idempotent
- Using
blockConcurrencyWhile()on every request limits throughput to ~200 req/sec — use it only for initialization - Workers on the free plan have a 10ms CPU time limit per request (not wall-clock time — I/O waiting is free)
- Cron trigger changes take up to 15 minutes to propagate globally
.dev.varsfile is for local secrets only and must be gitignored- The
envparameter is provided per-request by the runtime; avoid caching binding references or derived objects at module scope
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST run wrangler types to generate your Env interface — NEVER hand-write binding types)
(You MUST use wrangler.jsonc for new projects — Cloudflare recommends JSON config and some features are JSON-only)
(You MUST stream large request/response bodies — NEVER buffer entire payloads in memory (128 MB limit))
(You MUST avoid module-level mutable state — Workers reuse V8 isolates across requests, causing cross-request data leaks)
(You MUST use bindings for Cloudflare services (KV, D1, R2, Queues) — NEVER use REST APIs from within Workers)
Failure to follow these rules will result in memory crashes (buffering), data leaks (module state), type mismatches (hand-written Env), unnecessary latency (REST over bindings), and secret exposure (secrets in config).
</critical_reminders>
Files (skills)
-
examples
-
cloudflare-workers.md 889 B
# Cloudflare Workers Examples — Index > This file has been split into atomic concept files for progressive disclosure. See the individual files below. - [Core Setup & Configuration](core.md) — wrangler.jsonc, project init, basic fetch handler, secrets, multi-env config, CI/CD, testing - [KV Storage](kv.md) — KV binding, get/put/delete, typed responses, TTL, stale-while-revalidate caching - [D1 Database](d1.md) — D1 binding, parameterized queries, batch operations, migrations, CRUD API - [R2 Object Storage](r2.md) — R2 binding, file upload/download/delete with streaming, content-type validation - [Durable Objects](durable-objects.md) — DO classes, SQLite storage, RPC methods, rate limiter, WebSocket chat with hibernation - [Routing & Middleware](routing.md) — API framework, middleware, multi-handler workers, queues, cron, service bindings, Workers AI, streaming -
core.md 8.8 KB
# Cloudflare Workers — Core Setup & Configuration Examples > Core setup and configuration patterns for Cloudflare Workers projects. See [SKILL.md](../SKILL.md) for decision guidance. **Related examples:** - [KV Storage](kv.md) — Key-value storage patterns - [D1 Database](d1.md) — SQLite database patterns - [R2 Object Storage](r2.md) — File storage patterns - [Durable Objects](durable-objects.md) — Stateful edge compute - [Routing & Middleware](routing.md) — API framework and middleware --- ## Project Initialization ```bash # Create a new Workers project npm create cloudflare@latest -- my-worker # Or with a framework template (e.g., Hono) npm create hono@latest my-api # Select "cloudflare-workers" template ``` --- ## wrangler.jsonc Configuration ```jsonc // wrangler.jsonc — recommended for all new projects { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-api", "main": "src/index.ts", "compatibility_date": "2025-09-15", "compatibility_flags": ["nodejs_compat"], "observability": { "enabled": true, "head_sampling_rate": 1.0, }, "placement": { "mode": "smart", }, "upload_source_maps": true, } ``` **Why good:** JSON schema enables IDE autocomplete, `nodejs_compat` flag unlocks Node.js built-in modules, `observability` provides logs and traces in production, `smart` placement optimizes for latency, source maps enable readable stack traces ```toml # wrangler.toml — acceptable but not recommended for new projects name = "my-api" main = "src/index.ts" compatibility_date = "2024-01-01" ``` **Why bad:** TOML format misses JSON-only features, outdated compatibility_date misses runtime improvements and bug fixes, no observability configured (production is a black box), no source maps --- ## Type Generation ```bash # Generate Env interface from wrangler.jsonc bindings npx wrangler types # Creates worker-configuration.d.ts with all binding types ``` Always run `wrangler types` after changing bindings in `wrangler.jsonc`. Never hand-write the `Env` interface. --- ## Basic Fetch Handler ```typescript // src/index.ts import type { ExportedHandler } from "cloudflare:workers"; const CORS_HEADERS = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization", } as const; const ROUTES = { HEALTH: "/health", API_PREFIX: "/api/", } as const; export default { async fetch(request, env, ctx): Promise<Response> { const url = new URL(request.url); // Handle CORS preflight if (request.method === "OPTIONS") { return new Response(null, { headers: CORS_HEADERS }); } try { // Health check if (url.pathname === ROUTES.HEALTH) { return Response.json({ status: "healthy", timestamp: Date.now() }); } // API routes if (url.pathname.startsWith(ROUTES.API_PREFIX)) { const response = await handleApiRoute(url, request, env); // Add CORS headers to all API responses const headers = new Headers(response.headers); for (const [key, value] of Object.entries(CORS_HEADERS)) { headers.set(key, value); } return new Response(response.body, { status: response.status, headers, }); } return Response.json({ error: "Not Found" }, { status: 404 }); } catch (error) { console.error("Unhandled error:", error); return Response.json({ error: "Internal Server Error" }, { status: 500 }); } }, } satisfies ExportedHandler<Env>; async function handleApiRoute( url: URL, request: Request, env: Env, ): Promise<Response> { const path = url.pathname.slice(ROUTES.API_PREFIX.length); switch (path) { case "users": if (request.method === "GET") { return Response.json({ users: [] }); } return new Response("Method Not Allowed", { status: 405 }); default: return Response.json({ error: "Not Found" }, { status: 404 }); } } ``` **Why good:** `satisfies ExportedHandler<Env>` provides type checking while preserving literal types, named constants for routes and headers, proper error handling with try/catch, CORS support --- ## Module-Level State Anti-Pattern ```typescript // BAD: Module-level mutable state let requestCount = 0; // Leaks across requests export default { async fetch(request: Request, env: any, ctx: any) { requestCount++; // Cross-request data leak const body = await request.text(); // Buffers entire body in memory return new Response(body); }, }; ``` **Why bad:** Module-level `requestCount` leaks across requests (V8 isolate reuse), `env: any` loses type safety, buffering `request.text()` risks hitting 128 MB limit on large payloads --- ## Secrets Management ```bash # Set secrets (never in wrangler.jsonc or source code) npx wrangler secret put API_KEY npx wrangler secret put DATABASE_URL # Bulk upload secrets from JSON file ({"KEY": "value", ...}) npx wrangler secret bulk secrets.json # Local development secrets # Create .dev.vars (gitignored) echo 'API_KEY=dev-key-12345' >> .dev.vars ``` --- ## Multi-Environment Configuration ```jsonc // wrangler.jsonc { "name": "my-api", "main": "src/index.ts", "compatibility_date": "2025-09-15", "compatibility_flags": ["nodejs_compat"], "vars": { "ENVIRONMENT": "production", "LOG_LEVEL": "warn", }, "d1_databases": [ { "binding": "DB", "database_name": "my-app-prod", "database_id": "prod-id", }, ], "env": { "staging": { "name": "my-api-staging", "vars": { "ENVIRONMENT": "staging", "LOG_LEVEL": "debug", }, "d1_databases": [ { "binding": "DB", "database_name": "my-app-staging", "database_id": "staging-id", }, ], }, }, } ``` **Why good:** Bindings are re-declared per environment (they do not inherit), environment-specific vars override top-level, separate database per environment, secrets kept out of config ```bash # Deploy to staging npx wrangler deploy --env staging # Deploy to production (default) npx wrangler deploy ``` --- ## GitHub Actions CI/CD Deployment ```yaml # .github/workflows/deploy.yml name: Deploy Worker on: push: branches: [main] pull_request: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "22" cache: "npm" - run: npm ci - run: npx wrangler types - run: npx tsc --noEmit - run: npx vitest run deploy: needs: test if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "22" cache: "npm" - run: npm ci - name: Deploy to Cloudflare Workers uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} # Optionally deploy to staging first # command: deploy --env staging - name: Apply D1 Migrations uses: cloudflare/wrangler-action@v3 with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} command: d1 migrations apply my-database --remote ``` --- ## Testing with Workers Test Pool ```typescript // vitest.config.ts import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; export default defineWorkersConfig({ test: { poolOptions: { workers: { wrangler: { configPath: "./wrangler.jsonc" }, }, }, }, }); ``` ```typescript // src/__tests__/api.test.ts import { env, createExecutionContext, waitOnExecutionContext, } from "cloudflare:test"; import { describe, it, expect } from "vitest"; import worker from "../index"; describe("API Worker", () => { it("returns health check", async () => { const request = new Request("http://localhost/health"); const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); await waitOnExecutionContext(ctx); expect(response.status).toBe(200); const body = await response.json(); expect(body).toEqual({ status: "healthy" }); }); it("uses D1 database", async () => { // Real D1 binding available in test await env.DB.exec( "CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT)", ); await env.DB.prepare("INSERT INTO test (name) VALUES (?)") .bind("test-name") .run(); const result = await env.DB.prepare( "SELECT name FROM test WHERE id = 1", ).first<{ name: string }>(); expect(result?.name).toBe("test-name"); }); }); ``` **Why good:** Tests run inside actual Workers runtime (not Node.js), real bindings (KV, D1, R2) available in tests, isolated storage per test, catches runtime-specific issues early -
d1.md 6.6 KB
# Cloudflare Workers — D1 Database Examples > D1 database binding, parameterized SQL queries, batch operations, migrations, and CRUD API. See [SKILL.md](../SKILL.md) for decision guidance. **Related examples:** - [Core Setup & Configuration](core.md) — Project setup and wrangler.jsonc - [KV Storage](kv.md) — Key-value caching layer - [Routing & Middleware](routing.md) — API routing and middleware - [Durable Objects](durable-objects.md) — When to use DO vs D1 --- ## D1 Binding Configuration ```jsonc // wrangler.jsonc { "d1_databases": [ { "binding": "DB", "database_name": "my-app-db", "database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "migrations_dir": "migrations", }, ], } ``` --- ## Parameterized Queries ```typescript // Good Example — D1 with parameterized queries and batch interface User { id: number; email: string; name: string; created_at: string; } const DEFAULT_PAGE_SIZE = 20; async function getUserByEmail( db: D1Database, email: string, ): Promise<User | null> { const result = await db .prepare("SELECT id, email, name, created_at FROM users WHERE email = ?") .bind(email) .first<User>(); return result; } async function listUsers(db: D1Database, page: number): Promise<User[]> { const offset = (page - 1) * DEFAULT_PAGE_SIZE; const { results } = await db .prepare( "SELECT id, email, name, created_at FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?", ) .bind(DEFAULT_PAGE_SIZE, offset) .all<User>(); return results; } // Batch operations (executed sequentially, atomically) async function createUserWithProfile( db: D1Database, email: string, name: string, bio: string, ): Promise<void> { await db.batch([ db .prepare("INSERT INTO users (email, name) VALUES (?, ?)") .bind(email, name), db .prepare("INSERT INTO profiles (user_email, bio) VALUES (?, ?)") .bind(email, bio), ]); } ``` **Why good:** Parameterized queries prevent SQL injection, `first<T>()` for single row with type, `all<T>()` for multiple rows, `batch()` for atomic multi-statement operations, named page size constant ```typescript // Bad Example const result = await env.DB.exec( `SELECT * FROM users WHERE email = '${email}'`, // SQL injection ); ``` **Why bad:** String interpolation enables SQL injection, `exec()` does not support parameter binding, `SELECT *` fetches unnecessary columns --- ## D1 Migrations ```bash # Create a migration npx wrangler d1 migrations create my-app-db create_users_table # Apply locally npx wrangler d1 migrations apply my-app-db --local # Apply to production npx wrangler d1 migrations apply my-app-db --remote ``` ```sql -- migrations/0001_create_todos.sql CREATE TABLE IF NOT EXISTS todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, completed INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_todos_completed ON todos(completed); ``` --- ## Full CRUD API with D1 A complete CRUD API backed by D1 with parameterized queries and proper error handling (using Hono as the routing framework). ```jsonc // wrangler.jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "d1-api", "main": "src/index.ts", "compatibility_date": "2025-09-15", "compatibility_flags": ["nodejs_compat"], "d1_databases": [ { "binding": "DB", "database_name": "todo-app", "database_id": "your-database-id", "migrations_dir": "migrations", }, ], } ``` ```typescript // src/index.ts import { Hono } from "hono"; interface Todo { id: number; title: string; completed: number; created_at: string; updated_at: string; } const DEFAULT_PAGE_SIZE = 20; const MAX_TITLE_LENGTH = 500; const app = new Hono<{ Bindings: Env }>(); // List todos with pagination app.get("/todos", async (c) => { const page = Number(c.req.query("page") ?? 1); const offset = (page - 1) * DEFAULT_PAGE_SIZE; const { results: todos } = await c.env.DB.prepare( "SELECT * FROM todos ORDER BY created_at DESC LIMIT ? OFFSET ?", ) .bind(DEFAULT_PAGE_SIZE, offset) .all<Todo>(); const count = await c.env.DB.prepare( "SELECT COUNT(*) as total FROM todos", ).first<{ total: number }>(); return c.json({ todos, page, pageSize: DEFAULT_PAGE_SIZE, total: count?.total ?? 0, }); }); // Get single todo app.get("/todos/:id", async (c) => { const id = Number(c.req.param("id")); const todo = await c.env.DB.prepare("SELECT * FROM todos WHERE id = ?") .bind(id) .first<Todo>(); if (!todo) { return c.json({ error: "Todo not found" }, 404); } return c.json(todo); }); // Create todo app.post("/todos", async (c) => { const { title } = await c.req.json<{ title: string }>(); if (!title || title.length > MAX_TITLE_LENGTH) { return c.json({ error: "Invalid title" }, 400); } const result = await c.env.DB.prepare( "INSERT INTO todos (title) VALUES (?) RETURNING *", ) .bind(title) .first<Todo>(); return c.json(result, 201); }); // Update todo app.put("/todos/:id", async (c) => { const id = Number(c.req.param("id")); const { title, completed } = await c.req.json<{ title?: string; completed?: boolean; }>(); const updates: string[] = []; const values: unknown[] = []; if (title !== undefined) { if (title.length > MAX_TITLE_LENGTH) { return c.json({ error: "Title too long" }, 400); } updates.push("title = ?"); values.push(title); } if (completed !== undefined) { updates.push("completed = ?"); values.push(completed ? 1 : 0); } if (updates.length === 0) { return c.json({ error: "No fields to update" }, 400); } updates.push("updated_at = datetime('now')"); values.push(id); const result = await c.env.DB.prepare( `UPDATE todos SET ${updates.join(", ")} WHERE id = ? RETURNING *`, ) .bind(...values) .first<Todo>(); if (!result) { return c.json({ error: "Todo not found" }, 404); } return c.json(result); }); // Delete todo app.delete("/todos/:id", async (c) => { const id = Number(c.req.param("id")); const result = await c.env.DB.prepare("DELETE FROM todos WHERE id = ?") .bind(id) .run(); if (result.meta.changes === 0) { return c.json({ error: "Todo not found" }, 404); } return c.body(null, 204); }); export default app; ``` **Why good:** Full CRUD with parameterized queries, pagination with named constants, input validation, `RETURNING *` for created/updated records, proper 404/400/201/204 status codes -
durable-objects.md 8.9 KB
# Cloudflare Workers — Durable Objects Examples > Durable Object classes, SQLite storage, RPC methods, rate limiting, and WebSocket handling with hibernation. See [SKILL.md](../SKILL.md) for decision guidance. **Related examples:** - [Core Setup & Configuration](core.md) — Project setup and wrangler.jsonc - [D1 Database](d1.md) — When to use D1 vs Durable Objects - [Routing & Middleware](routing.md) — API routing and middleware - [KV Storage](kv.md) — Eventually consistent key-value storage --- ## Durable Object Binding Configuration ```jsonc // wrangler.jsonc { "durable_objects": { "bindings": [ { "name": "COUNTER", "class_name": "Counter", }, ], }, "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Counter"] }], } ``` --- ## Counter with RPC and SQLite ```typescript // src/counter.ts — Durable Object with RPC and SQLite import { DurableObject } from "cloudflare:workers"; export class Counter extends DurableObject<Env> { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); // Run migrations before processing any requests ctx.blockConcurrencyWhile(async () => { this.ctx.storage.sql.exec(` CREATE TABLE IF NOT EXISTS counters ( name TEXT PRIMARY KEY, value INTEGER NOT NULL DEFAULT 0 ) `); }); } // RPC methods — automatically exposed, type-safe async increment(name: string, amount: number = 1): Promise<number> { const result = this.ctx.storage.sql.exec<{ value: number }>( `INSERT INTO counters (name, value) VALUES (?, ?) ON CONFLICT(name) DO UPDATE SET value = value + ? RETURNING value`, name, amount, amount, ); return [...result][0].value; } async getCount(name: string): Promise<number> { const result = this.ctx.storage.sql.exec<{ value: number }>( "SELECT value FROM counters WHERE name = ?", name, ); const rows = [...result]; return rows.length > 0 ? rows[0].value : 0; } } // src/index.ts — Calling a Durable Object export default { async fetch(request, env, ctx): Promise<Response> { const id = env.COUNTER.idFromName("global"); const stub = env.COUNTER.get(id); const count = await stub.increment("page-views"); return Response.json({ count }); }, } satisfies ExportedHandler<Env>; ``` **Why good:** SQLite storage for durable persistence, `blockConcurrencyWhile` for safe schema migration, RPC methods instead of fetch handler (type-safe, ergonomic), `idFromName` for deterministic routing, SQL with parameterized queries ```typescript // Bad Example — Single global Durable Object const id = env.STATE.idFromName("global-state"); // Everything goes through one DO const stub = env.STATE.get(id); await stub.fetch(request); // Using fetch instead of RPC ``` **Why bad:** Single global DO becomes a bottleneck (~1000 req/sec max), `fetch()` requires manual request/response parsing — use RPC methods instead --- ## Rate Limiter with SQLite Storage A per-key rate limiter using Durable Objects for strong consistency. ```jsonc // wrangler.jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "rate-limiter", "main": "src/index.ts", "compatibility_date": "2025-09-15", "compatibility_flags": ["nodejs_compat"], "durable_objects": { "bindings": [{ "name": "RATE_LIMITER", "class_name": "RateLimiter" }], }, "migrations": [{ "tag": "v1", "new_sqlite_classes": ["RateLimiter"] }], } ``` ```typescript // src/rate-limiter.ts import { DurableObject } from "cloudflare:workers"; const WINDOW_SIZE_MS = 60_000; // 1 minute window const MAX_REQUESTS = 100; // 100 requests per window interface RateLimitResult { allowed: boolean; remaining: number; resetAt: number; } export class RateLimiter extends DurableObject<Env> { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); ctx.blockConcurrencyWhile(async () => { this.ctx.storage.sql.exec(` CREATE TABLE IF NOT EXISTS requests ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp INTEGER NOT NULL ) `); this.ctx.storage.sql.exec( "CREATE INDEX IF NOT EXISTS idx_requests_timestamp ON requests(timestamp)", ); }); } async checkLimit(): Promise<RateLimitResult> { const now = Date.now(); const windowStart = now - WINDOW_SIZE_MS; // Clean old entries and count in one batch (write coalescing) this.ctx.storage.sql.exec( "DELETE FROM requests WHERE timestamp < ?", windowStart, ); const countResult = this.ctx.storage.sql.exec<{ count: number }>( "SELECT COUNT(*) as count FROM requests WHERE timestamp >= ?", windowStart, ); const count = [...countResult][0].count; if (count >= MAX_REQUESTS) { // Find when the oldest request in the window expires const oldestResult = this.ctx.storage.sql.exec<{ timestamp: number }>( "SELECT MIN(timestamp) as timestamp FROM requests WHERE timestamp >= ?", windowStart, ); const oldest = [...oldestResult][0].timestamp; return { allowed: false, remaining: 0, resetAt: oldest + WINDOW_SIZE_MS, }; } // Record this request this.ctx.storage.sql.exec( "INSERT INTO requests (timestamp) VALUES (?)", now, ); return { allowed: true, remaining: MAX_REQUESTS - count - 1, resetAt: now + WINDOW_SIZE_MS, }; } } ``` ```typescript // src/index.ts import type { ExportedHandler } from "cloudflare:workers"; export { RateLimiter } from "./rate-limiter"; export default { async fetch(request, env, ctx): Promise<Response> { // Rate limit by IP address (one DO per IP) const ip = request.headers.get("cf-connecting-ip") ?? "unknown"; const id = env.RATE_LIMITER.idFromName(ip); const limiter = env.RATE_LIMITER.get(id); const result = await limiter.checkLimit(); if (!result.allowed) { return Response.json( { error: "Rate limit exceeded" }, { status: 429, headers: { "Retry-After": String( Math.ceil((result.resetAt - Date.now()) / 1_000), ), "X-RateLimit-Remaining": "0", "X-RateLimit-Reset": String(result.resetAt), }, }, ); } // Process the actual request return Response.json( { message: "OK", rateLimitRemaining: result.remaining, }, { headers: { "X-RateLimit-Remaining": String(result.remaining), "X-RateLimit-Reset": String(result.resetAt), }, }, ); }, } satisfies ExportedHandler<Env>; ``` **Why good:** Per-IP sharding via `idFromName`, SQLite for durable rate limit tracking, sliding window with cleanup, proper rate limit headers --- ## WebSocket Chat Room with Hibernation Persistent WebSocket connections using the Hibernatable WebSocket API for cost-effective real-time communication. ```typescript // src/chat-room.ts — Hibernatable WebSockets import { DurableObject } from "cloudflare:workers"; interface ConnectionState { userId: string; joinedAt: number; } export class ChatRoom extends DurableObject<Env> { async fetch(request: Request): Promise<Response> { if (request.headers.get("Upgrade") !== "websocket") { return new Response("Expected WebSocket", { status: 426 }); } const pair = new WebSocketPair(); const [client, server] = Object.values(pair); // Accept with hibernation support this.ctx.acceptWebSocket(server); // Attach metadata that survives hibernation const state: ConnectionState = { userId: new URL(request.url).searchParams.get("userId") ?? "anonymous", joinedAt: Date.now(), }; server.serializeAttachment(state); return new Response(null, { status: 101, webSocket: client }); } // Called when a message arrives (even after hibernation) async webSocketMessage( ws: WebSocket, message: string | ArrayBuffer, ): Promise<void> { const state = ws.deserializeAttachment() as ConnectionState; const data = typeof message === "string" ? message : new TextDecoder().decode(message); // Broadcast to all connected clients for (const client of this.ctx.getWebSockets()) { if (client !== ws) { client.send( JSON.stringify({ userId: state.userId, message: data, timestamp: Date.now(), }), ); } } } // MUST reciprocate close to avoid 1006 errors async webSocketClose( ws: WebSocket, code: number, reason: string, ): Promise<void> { ws.close(code, reason); } async webSocketError(ws: WebSocket, error: unknown): Promise<void> { ws.close(1011, "Internal error"); } } ``` **Why good:** Hibernatable API keeps connections alive while DO sleeps (reduces cost), `serializeAttachment` preserves metadata across hibernation, broadcasts to all clients via `getWebSockets()`, reciprocates close to prevent 1006 errors -
kv.md 5.4 KB
# Cloudflare Workers — KV Storage Examples > KV namespace binding, get/put/delete/list operations, caching patterns, and stale-while-revalidate. See [SKILL.md](../SKILL.md) for decision guidance. **Related examples:** - [Core Setup & Configuration](core.md) — Project setup and wrangler.jsonc - [D1 Database](d1.md) — Relational data with SQL - [R2 Object Storage](r2.md) — File/blob storage - [Routing & Middleware](routing.md) — API routing and middleware --- ## KV Binding Configuration ```jsonc // wrangler.jsonc { "kv_namespaces": [ { "binding": "CACHE", "id": "abc123def456", }, ], } ``` --- ## Basic KV Operations ```typescript // Good Example — KV with typed responses and TTL const CACHE_TTL_SECONDS = 3_600; // 1 hour interface UserProfile { name: string; email: string; } async function getCachedProfile( kv: KVNamespace, userId: string, ): Promise<UserProfile | null> { return kv.get<UserProfile>(`user:${userId}`, "json"); } async function setCachedProfile( kv: KVNamespace, userId: string, profile: UserProfile, ): Promise<void> { await kv.put(`user:${userId}`, JSON.stringify(profile), { expirationTtl: CACHE_TTL_SECONDS, }); } // In fetch handler export default { async fetch(request, env, ctx): Promise<Response> { const userId = new URL(request.url).searchParams.get("id"); if (!userId) { return new Response("Missing id", { status: 400 }); } const cached = await getCachedProfile(env.CACHE, userId); if (cached) { return Response.json(cached); } // Fetch from origin, cache in background const profile = await fetchProfileFromOrigin(userId); ctx.waitUntil(setCachedProfile(env.CACHE, userId, profile)); return Response.json(profile); }, } satisfies ExportedHandler<Env>; ``` **Why good:** Typed `get<T>` with `"json"` return type, named TTL constant, `ctx.waitUntil()` for non-blocking cache writes, key prefix pattern for namespacing ```typescript // Bad Example const value = await env.CACHE.get("key"); // Untyped, returns string await env.CACHE.put("key", data); // No TTL — data never expires ``` **Why bad:** Untyped get returns `string | null` requiring manual parsing, no TTL means stale data persists forever, no key prefix for organization --- ## Stale-While-Revalidate Cache A Worker that caches expensive API responses in KV with TTL and stale-while-revalidate pattern. ```jsonc // wrangler.jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "cache-worker", "main": "src/index.ts", "compatibility_date": "2025-09-15", "compatibility_flags": ["nodejs_compat"], "kv_namespaces": [ { "binding": "CACHE", "id": "your-kv-namespace-id", }, ], "vars": { "UPSTREAM_API": "https://api.example.com", }, } ``` ```typescript // src/index.ts import type { ExportedHandler } from "cloudflare:workers"; const CACHE_TTL_SECONDS = 300; // 5 minutes const STALE_TTL_SECONDS = 3_600; // 1 hour (serve stale while refreshing) const CACHE_KEY_PREFIX = "api-cache:"; interface CacheEntry<T> { data: T; cachedAt: number; ttl: number; } async function getCached<T>( kv: KVNamespace, key: string, ): Promise<{ data: T; isStale: boolean } | null> { const entry = await kv.get<CacheEntry<T>>( `${CACHE_KEY_PREFIX}${key}`, "json", ); if (!entry) return null; const age = (Date.now() - entry.cachedAt) / 1_000; const isStale = age > entry.ttl; return { data: entry.data, isStale }; } async function setCache<T>( kv: KVNamespace, key: string, data: T, ): Promise<void> { const entry: CacheEntry<T> = { data, cachedAt: Date.now(), ttl: CACHE_TTL_SECONDS, }; await kv.put(`${CACHE_KEY_PREFIX}${key}`, JSON.stringify(entry), { expirationTtl: STALE_TTL_SECONDS, }); } export default { async fetch(request, env, ctx): Promise<Response> { const url = new URL(request.url); const cacheKey = url.pathname + url.search; // Check cache const cached = await getCached<unknown>(env.CACHE, cacheKey); if (cached && !cached.isStale) { // Fresh cache hit return Response.json(cached.data, { headers: { "X-Cache": "HIT" }, }); } if (cached?.isStale) { // Stale: return stale data, refresh in background ctx.waitUntil(refreshCache(env, cacheKey, url)); return Response.json(cached.data, { headers: { "X-Cache": "STALE" }, }); } // Cache miss: fetch from upstream const data = await fetchUpstream(env, url); ctx.waitUntil(setCache(env.CACHE, cacheKey, data)); return Response.json(data, { headers: { "X-Cache": "MISS" }, }); }, } satisfies ExportedHandler<Env>; async function fetchUpstream(env: Env, url: URL): Promise<unknown> { const upstream = `${env.UPSTREAM_API}${url.pathname}${url.search}`; const response = await fetch(upstream); if (!response.ok) { throw new Error(`Upstream error: ${response.status}`); } return response.json(); } async function refreshCache(env: Env, key: string, url: URL): Promise<void> { try { const data = await fetchUpstream(env, url); await setCache(env.CACHE, key, data); } catch (error) { console.error("Background refresh failed:", error); } } ``` **Why good:** Two-tier TTL (fresh vs stale), stale data served immediately while background refresh happens via `ctx.waitUntil()`, X-Cache headers for debugging, typed cache entries -
r2.md 5.2 KB
# Cloudflare Workers — R2 Object Storage Examples > R2 bucket binding, file upload/download/delete, streaming, content-type handling, and list operations. See [SKILL.md](../SKILL.md) for decision guidance. **Related examples:** - [Core Setup & Configuration](core.md) — Project setup and wrangler.jsonc - [KV Storage](kv.md) — Key-value caching layer - [D1 Database](d1.md) — Relational data with SQL - [Routing & Middleware](routing.md) — API routing and middleware --- ## R2 Binding Configuration ```jsonc // wrangler.jsonc { "r2_buckets": [ { "binding": "BUCKET", "bucket_name": "my-files", }, ], } ``` --- ## Basic R2 Operations with Streaming ```typescript // Good Example — R2 file operations with streaming import type { ExportedHandler } from "cloudflare:workers"; const MAX_UPLOAD_SIZE = 10 * 1024 * 1024; // 10 MB export default { async fetch(request, env, ctx): Promise<Response> { const url = new URL(request.url); const key = url.pathname.slice(1); // Remove leading / switch (request.method) { case "GET": { const object = await env.BUCKET.get(key); if (!object) { return new Response("Not Found", { status: 404 }); } const headers = new Headers(); object.writeHttpMetadata(headers); headers.set("etag", object.httpEtag); // Stream body directly — never buffer return new Response(object.body, { headers }); } case "PUT": { const contentLength = Number( request.headers.get("content-length") ?? 0, ); if (contentLength > MAX_UPLOAD_SIZE) { return new Response("File too large", { status: 413 }); } await env.BUCKET.put(key, request.body, { httpMetadata: request.headers, }); return new Response(`Uploaded ${key}`, { status: 201 }); } case "DELETE": { await env.BUCKET.delete(key); return new Response(null, { status: 204 }); } default: return new Response("Method Not Allowed", { status: 405 }); } }, } satisfies ExportedHandler<Env>; ``` **Why good:** Streams R2 object body directly to response (no buffering), sets HTTP metadata and ETag for caching, validates upload size before accepting, uses `request.body` stream for uploads, named size constant ```typescript // Bad Example const object = await env.BUCKET.get(key); const data = await object.arrayBuffer(); // Buffers entire file in memory return new Response(data); ``` **Why bad:** `arrayBuffer()` loads entire file into memory — a 100 MB file exceeds the 128 MB Worker limit and crashes --- ## File Storage Service An R2-backed file upload/download service (using Hono) with content-type validation and metadata. ```typescript // src/index.ts import { Hono } from "hono"; const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50 MB const ALLOWED_CONTENT_TYPES = new Set([ "image/jpeg", "image/png", "image/webp", "application/pdf", "text/plain", ]); const app = new Hono<{ Bindings: Env }>(); // Upload file app.put("/files/:key{.+}", async (c) => { const key = c.req.param("key"); const contentType = c.req.header("content-type") ?? "application/octet-stream"; const contentLength = Number(c.req.header("content-length") ?? 0); if (contentLength > MAX_FILE_SIZE) { return c.json({ error: "File too large", maxSize: MAX_FILE_SIZE }, 413); } if (!ALLOWED_CONTENT_TYPES.has(contentType)) { return c.json({ error: "Content type not allowed" }, 415); } const object = await c.env.BUCKET.put(key, c.req.raw.body, { httpMetadata: { contentType }, customMetadata: { uploadedAt: new Date().toISOString(), uploadedBy: c.req.header("x-user-id") ?? "anonymous", }, }); return c.json( { key: object?.key, size: object?.size, etag: object?.etag, }, 201, ); }); // Download file (streaming) app.get("/files/:key{.+}", async (c) => { const key = c.req.param("key"); const object = await c.env.BUCKET.get(key); if (!object) { return c.json({ error: "File not found" }, 404); } const headers = new Headers(); object.writeHttpMetadata(headers); headers.set("etag", object.httpEtag); headers.set("cache-control", "public, max-age=31536000, immutable"); // Stream body directly to response return new Response(object.body, { headers }); }); // List files with prefix app.get("/files", async (c) => { const prefix = c.req.query("prefix") ?? ""; const cursor = c.req.query("cursor"); const listed = await c.env.BUCKET.list({ prefix, cursor: cursor ?? undefined, limit: 100, }); return c.json({ objects: listed.objects.map((obj) => ({ key: obj.key, size: obj.size, uploaded: obj.uploaded, etag: obj.etag, })), truncated: listed.truncated, cursor: listed.truncated ? listed.cursor : undefined, }); }); // Delete file app.delete("/files/:key{.+}", async (c) => { const key = c.req.param("key"); await c.env.BUCKET.delete(key); return c.body(null, 204); }); export default app; ``` **Why good:** Content-type validation before upload, custom metadata for auditing, streaming download with cache headers, cursor-based pagination for listing, proper HTTP status codes -
routing.md 13 KB
# Cloudflare Workers — Routing & Middleware Examples > API framework integration, middleware, multi-handler workers (scheduled, queue), and service bindings. See [SKILL.md](../SKILL.md) for decision guidance. **Related examples:** - [Core Setup & Configuration](core.md) — Project setup and wrangler.jsonc - [D1 Database](d1.md) — D1 CRUD API - [R2 Object Storage](r2.md) — R2 file service - [KV Storage](kv.md) — KV caching patterns - [Durable Objects](durable-objects.md) — Stateful edge compute --- ## Basic Framework Setup (Hono) ```typescript // src/index.ts — Hono with typed bindings import { Hono } from "hono"; import { cors } from "hono/cors"; import { logger } from "hono/logger"; // Use wrangler types-generated Env const app = new Hono<{ Bindings: Env }>(); app.use("*", logger()); app.use("*", cors()); app.get("/health", (c) => { return c.json({ status: "healthy" }); }); app.get("/users/:id", async (c) => { const id = c.req.param("id"); const user = await c.env.DB.prepare( "SELECT id, email, name FROM users WHERE id = ?", ) .bind(id) .first(); if (!user) { return c.json({ error: "User not found" }, 404); } return c.json(user); }); app.post("/upload/:key", async (c) => { const key = c.req.param("key"); await c.env.BUCKET.put(key, c.req.raw.body); return c.json({ uploaded: key }, 201); }); // Export for Workers runtime export default app; ``` **Why good:** `Hono<{ Bindings: Env }>` provides type-safe access to all bindings via `c.env`, built-in middleware for cors/logging, clean route definitions, direct D1/R2 binding access through context --- ## Framework with Multiple Handlers (Scheduled, Queue) ```typescript // When you need fetch + scheduled + queue handlers const app = new Hono<{ Bindings: Env }>(); // ... routes ... export default { fetch: app.fetch, async scheduled(event, env, ctx) { // Cron trigger handler await cleanupExpiredData(env.DB); }, async queue(batch, env, ctx) { // Queue consumer handler for (const message of batch.messages) { // process messages } }, } satisfies ExportedHandler<Env>; ``` **Why good:** Hono handles HTTP routing while other event handlers (scheduled, queue) are exported alongside `app.fetch` --- ## Production API with D1, KV, Middleware A production-ready Workers API (using Hono) with D1, KV caching, middleware, error handling, and structured logging. ```jsonc // wrangler.jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "hono-api", "main": "src/index.ts", "compatibility_date": "2025-09-15", "compatibility_flags": ["nodejs_compat"], "observability": { "enabled": true, "head_sampling_rate": 1.0, }, "placement": { "mode": "smart" }, "upload_source_maps": true, "d1_databases": [ { "binding": "DB", "database_name": "hono-api-db", "database_id": "your-id", "migrations_dir": "migrations", }, ], "kv_namespaces": [{ "binding": "CACHE", "id": "your-kv-id" }], "vars": { "ENVIRONMENT": "production", }, "triggers": { "crons": ["0 2 * * *"], }, } ``` ```typescript // src/index.ts import { Hono } from "hono"; import { cors } from "hono/cors"; import { secureHeaders } from "hono/secure-headers"; import { HTTPException } from "hono/http-exception"; import { userRoutes } from "./routes/users"; import { healthRoutes } from "./routes/health"; const app = new Hono<{ Bindings: Env }>(); // Global middleware app.use("*", cors()); app.use("*", secureHeaders()); // Structured request logging app.use("*", async (c, next) => { const start = Date.now(); await next(); const duration = Date.now() - start; console.log( JSON.stringify({ method: c.req.method, path: c.req.path, status: c.res.status, duration, env: c.env.ENVIRONMENT, }), ); }); // Global error handler app.onError((error, c) => { if (error instanceof HTTPException) { return c.json({ error: error.message }, error.status); } console.error("Unhandled error:", error); return c.json({ error: "Internal Server Error" }, 500); }); // Mount routes app.route("/", healthRoutes); app.route("/api/users", userRoutes); // 404 fallback app.notFound((c) => c.json({ error: "Not Found" }, 404)); // Export with additional handlers export default { fetch: app.fetch, async scheduled(event, env, ctx): Promise<void> { if (event.cron === "0 2 * * *") { // Daily cleanup at 2 AM ctx.waitUntil( env.DB.prepare( "DELETE FROM sessions WHERE expires_at < datetime('now')", ).run(), ); } }, } satisfies ExportedHandler<Env>; ``` ```typescript // src/routes/health.ts import { Hono } from "hono"; const healthRoutes = new Hono<{ Bindings: Env }>(); healthRoutes.get("/health", async (c) => { const checks: Record<string, string> = {}; try { await c.env.DB.prepare("SELECT 1").first(); checks.database = "healthy"; } catch { checks.database = "unhealthy"; } try { await c.env.CACHE.get("health-check"); checks.cache = "healthy"; } catch { checks.cache = "unhealthy"; } const healthy = Object.values(checks).every((v) => v === "healthy"); return c.json( { status: healthy ? "healthy" : "degraded", checks }, healthy ? 200 : 503, ); }); export { healthRoutes }; ``` ```typescript // src/routes/users.ts import { Hono } from "hono"; import { HTTPException } from "hono/http-exception"; interface User { id: number; email: string; name: string; created_at: string; } const CACHE_TTL_SECONDS = 300; const DEFAULT_PAGE_SIZE = 20; const userRoutes = new Hono<{ Bindings: Env }>(); // List users with KV caching userRoutes.get("/", async (c) => { const page = Number(c.req.query("page") ?? 1); const cacheKey = `users:page:${page}`; // Check KV cache first const cached = await c.env.CACHE.get<{ users: User[]; total: number }>( cacheKey, "json", ); if (cached) { return c.json({ ...cached, cached: true }); } const offset = (page - 1) * DEFAULT_PAGE_SIZE; const { results: users } = await c.env.DB.prepare( "SELECT id, email, name, created_at FROM users ORDER BY id DESC LIMIT ? OFFSET ?", ) .bind(DEFAULT_PAGE_SIZE, offset) .all<User>(); const count = await c.env.DB.prepare( "SELECT COUNT(*) as total FROM users", ).first<{ total: number }>(); const data = { users, total: count?.total ?? 0, page, pageSize: DEFAULT_PAGE_SIZE, }; // Cache in background c.executionCtx.waitUntil( c.env.CACHE.put(cacheKey, JSON.stringify(data), { expirationTtl: CACHE_TTL_SECONDS, }), ); return c.json({ ...data, cached: false }); }); // Get single user userRoutes.get("/:id", async (c) => { const id = Number(c.req.param("id")); const user = await c.env.DB.prepare( "SELECT id, email, name, created_at FROM users WHERE id = ?", ) .bind(id) .first<User>(); if (!user) { throw new HTTPException(404, { message: "User not found" }); } return c.json(user); }); // Create user userRoutes.post("/", async (c) => { const { email, name } = await c.req.json<{ email: string; name: string }>(); if (!email || !name) { throw new HTTPException(400, { message: "email and name are required" }); } const user = await c.env.DB.prepare( "INSERT INTO users (email, name) VALUES (?, ?) RETURNING id, email, name, created_at", ) .bind(email, name) .first<User>(); return c.json(user, 201); }); export { userRoutes }; ``` **Why good:** Structured logging with JSON, secure headers middleware, global error handler with HTTPException, health checks for all dependencies, KV caching with background refresh, modular route files --- ## Queues (Producer and Consumer) ```jsonc // wrangler.jsonc { "queues": { "producers": [ { "binding": "EMAIL_QUEUE", "queue": "email-notifications", }, ], "consumers": [ { "queue": "email-notifications", "max_batch_size": 10, "max_batch_timeout": 30, "max_retries": 3, "dead_letter_queue": "email-dlq", "max_concurrency": 5, }, ], }, } ``` ```typescript // Good Example — Queue producer and consumer interface EmailMessage { to: string; subject: string; body: string; } export default { // Producer: enqueue messages from HTTP requests async fetch(request, env, ctx): Promise<Response> { const message: EmailMessage = await request.json(); await env.EMAIL_QUEUE.send(message); return Response.json({ queued: true }, { status: 202 }); }, // Consumer: process message batches async queue(batch, env, ctx): Promise<void> { for (const message of batch.messages) { try { const email = message.body as EmailMessage; await sendEmail(env, email); message.ack(); // Acknowledge successful processing } catch (error) { message.retry(); // Retry on failure } } }, } satisfies ExportedHandler<Env>; ``` **Why good:** Typed message interface, per-message ack/retry for fine-grained control, dead-letter queue for poison messages, batch processing for efficiency, 202 status for async acceptance --- ## Cron Triggers (Scheduled Workers) ```jsonc // wrangler.jsonc { "triggers": { "crons": ["0 */6 * * *", "0 0 * * 1"], }, } ``` ```typescript // Good Example — Scheduled handler export default { async fetch(request, env, ctx): Promise<Response> { return new Response("OK"); }, async scheduled(event, env, ctx): Promise<void> { switch (event.cron) { case "0 */6 * * *": // Every 6 hours: clean expired cache ctx.waitUntil(cleanExpiredEntries(env.DB)); break; case "0 0 * * 1": // Every Monday: generate weekly report ctx.waitUntil(generateWeeklyReport(env)); break; } }, } satisfies ExportedHandler<Env>; ``` **Why good:** Switch on `event.cron` to handle multiple schedules, `ctx.waitUntil()` for background work, separate concerns per cron expression ```bash # Test cron locally npx wrangler dev --test-scheduled # Trigger manually curl "http://localhost:8787/__scheduled?cron=0+*/6+*+*+*" ``` --- ## Service Bindings (Worker-to-Worker RPC) ```jsonc // wrangler.jsonc (caller worker) { "services": [ { "binding": "AUTH_SERVICE", "service": "auth-worker", }, ], } ``` ```typescript // Good Example — Service binding call export default { async fetch(request, env, ctx): Promise<Response> { // Call another Worker via service binding (no HTTP overhead) const authResponse = await env.AUTH_SERVICE.fetch( new Request("https://auth/verify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token: request.headers.get("authorization") }), }), ); if (!authResponse.ok) { return new Response("Unauthorized", { status: 401 }); } return new Response("Authenticated!"); }, } satisfies ExportedHandler<Env>; ``` **Why good:** Service binding bypasses public internet, no DNS resolution or TLS overhead, the call is in-process within Cloudflare's network --- ## Workers AI ```jsonc // wrangler.jsonc { "ai": { "binding": "AI", }, } ``` ```typescript // Good Example — Workers AI text generation const MAX_PROMPT_LENGTH = 4_000; const AI_MODEL = "@cf/meta/llama-3.3-70b-instruct-fp8-fast"; export default { async fetch(request, env, ctx): Promise<Response> { const { prompt } = await request.json<{ prompt: string }>(); if (prompt.length > MAX_PROMPT_LENGTH) { return new Response("Prompt too long", { status: 400 }); } const result = await env.AI.run(AI_MODEL, { messages: [ { role: "system", content: "You are a helpful assistant." }, { role: "user", content: prompt }, ], }); return Response.json(result); }, } satisfies ExportedHandler<Env>; ``` **Why good:** Named model constant, input validation before inference, structured message format, type-safe AI binding --- ## Streaming Large Responses ```typescript // Good Example — Stream a large R2 file export default { async fetch(request, env, ctx): Promise<Response> { const object = await env.BUCKET.get("large-file.csv"); if (!object) { return new Response("Not Found", { status: 404 }); } // Stream body directly — never call .text() or .arrayBuffer() on large files return new Response(object.body, { headers: { "Content-Type": "text/csv", "Content-Disposition": 'attachment; filename="large-file.csv"', }, }); }, } satisfies ExportedHandler<Env>; ``` ```typescript // Good Example — Transform stream pipeline async function transformResponse(response: Response): Promise<Response> { const { readable, writable } = new TransformStream({ transform(chunk, controller) { // Process each chunk without buffering the whole body controller.enqueue(chunk); }, }); response.body?.pipeTo(writable); return new Response(readable, response); } ``` **Why good:** Processes data chunk-by-chunk, never loads entire payload into memory, compatible with R2/external fetch responses
-
-
reference.md 10.1 KB
# Cloudflare Workers Quick Reference ## Wrangler CLI Commands ### Development ```bash # Start local dev server (http://localhost:8787) npx wrangler dev # Dev with remote bindings (real KV/D1/R2) npx wrangler dev --remote # Dev with cron trigger testing npx wrangler dev --test-scheduled # Trigger scheduled handler manually curl "http://localhost:8787/__scheduled?cron=0+*/6+*+*+*" ``` ### Deployment ```bash # Deploy to production npx wrangler deploy # Deploy to specific environment npx wrangler deploy --env staging # Bulk upload secrets from JSON file npx wrangler secret bulk secrets.json # Tail live logs npx wrangler tail npx wrangler tail --env staging ``` ### Type Generation ```bash # Generate Env interface from wrangler.jsonc npx wrangler types # Output: worker-configuration.d.ts ``` ### Secrets ```bash # Set a secret (interactive prompt) npx wrangler secret put API_KEY # Set secret for specific environment npx wrangler secret put API_KEY --env staging # List secrets npx wrangler secret list # Delete a secret npx wrangler secret delete API_KEY ``` ### D1 Database ```bash # Create database npx wrangler d1 create my-database # Create migration npx wrangler d1 migrations create my-database migration_name # Apply migrations locally npx wrangler d1 migrations apply my-database --local # Apply migrations remotely npx wrangler d1 migrations apply my-database --remote # Execute SQL npx wrangler d1 execute my-database --command "SELECT * FROM users" --remote # Export database npx wrangler d1 export my-database --remote --output backup.sql ``` ### KV Namespace ```bash # Create namespace npx wrangler kv namespace create CACHE # List namespaces npx wrangler kv namespace list # Put a value npx wrangler kv key put --namespace-id <id> "key" "value" # Get a value npx wrangler kv key get --namespace-id <id> "key" # List keys npx wrangler kv key list --namespace-id <id> ``` ### R2 Bucket ```bash # Create bucket npx wrangler r2 bucket create my-files # List buckets npx wrangler r2 bucket list # Upload object npx wrangler r2 object put my-files/path/to/file.txt --file ./local-file.txt # Download object npx wrangler r2 object get my-files/path/to/file.txt # Delete object npx wrangler r2 object delete my-files/path/to/file.txt ``` ### Queues ```bash # Create queue npx wrangler queues create my-queue # List queues npx wrangler queues list # Delete queue npx wrangler queues delete my-queue ``` --- ## Handler Signatures ### Fetch Handler (HTTP) ```typescript export default { async fetch( request: Request, env: Env, ctx: ExecutionContext, ): Promise<Response> { return new Response("OK"); }, } satisfies ExportedHandler<Env>; ``` ### Scheduled Handler (Cron) ```typescript export default { async scheduled( event: ScheduledController, env: Env, ctx: ExecutionContext, ): Promise<void> { // event.cron — cron expression that triggered // event.scheduledTime — epoch ms when cron was supposed to run }, } satisfies ExportedHandler<Env>; ``` ### Queue Handler (Consumer) ```typescript export default { async queue( batch: MessageBatch<unknown>, env: Env, ctx: ExecutionContext, ): Promise<void> { for (const message of batch.messages) { // message.body — deserialized message content // message.id — unique message ID // message.timestamp — when message was sent message.ack(); // acknowledge message.retry(); // re-queue for retry } }, } satisfies ExportedHandler<Env>; ``` --- ## Binding Type Reference ### KV Namespace ```typescript interface KVNamespace { get(key: string, type?: "text"): Promise<string | null>; get<T>(key: string, type: "json"): Promise<T | null>; get(key: string, type: "arrayBuffer"): Promise<ArrayBuffer | null>; get(key: string, type: "stream"): Promise<ReadableStream | null>; put( key: string, value: string | ArrayBuffer | ReadableStream, options?: { expirationTtl?: number; // seconds until expiry expiration?: number; // unix epoch seconds metadata?: Record<string, unknown>; }, ): Promise<void>; delete(key: string): Promise<void>; list(options?: { prefix?: string; limit?: number; // default 1000, max 1000 cursor?: string; }): Promise<KVNamespaceListResult>; getWithMetadata<T>( key: string, type: "json", ): Promise<{ value: T | null; metadata: Record<string, unknown> | null; }>; } ``` ### D1 Database ```typescript interface D1Database { prepare(query: string): D1PreparedStatement; batch<T = unknown>(statements: D1PreparedStatement[]): Promise<D1Result<T>[]>; exec(query: string): Promise<D1ExecResult>; withSession(constraint?: string): D1DatabaseSession; } interface D1DatabaseSession { prepare(query: string): D1PreparedStatement; batch<T = unknown>(statements: D1PreparedStatement[]): Promise<D1Result<T>[]>; getBookmark(): string; } interface D1PreparedStatement { bind(...values: unknown[]): D1PreparedStatement; first<T = unknown>(column?: string): Promise<T | null>; all<T = unknown>(): Promise<D1Result<T>>; run(): Promise<D1Result>; raw<T = unknown[]>(): Promise<T[]>; } interface D1Result<T = unknown> { results: T[]; success: boolean; meta: { changed_db: boolean; changes: number; last_row_id: number; duration: number; rows_read: number; rows_written: number; }; } ``` ### R2 Bucket ```typescript interface R2Bucket { get(key: string, options?: R2GetOptions): Promise<R2ObjectBody | null>; put( key: string, value: ReadableStream | ArrayBuffer | string | Blob | null, options?: R2PutOptions, ): Promise<R2Object | null>; delete(keys: string | string[]): Promise<void>; list(options?: R2ListOptions): Promise<R2Objects>; head(key: string): Promise<R2Object | null>; createMultipartUpload( key: string, options?: R2MultipartOptions, ): Promise<R2MultipartUpload>; } interface R2ObjectBody extends R2Object { body: ReadableStream; bodyUsed: boolean; arrayBuffer(): Promise<ArrayBuffer>; text(): Promise<string>; json<T>(): Promise<T>; writeHttpMetadata(headers: Headers): void; } ``` ### Queue Producer ```typescript interface Queue<Body = unknown> { send( message: Body, options?: { contentType?: string; delaySeconds?: number }, ): Promise<void>; sendBatch( messages: { body: Body; contentType?: string; delaySeconds?: number }[], ): Promise<void>; } ``` ### Durable Object Namespace ```typescript interface DurableObjectNamespace<T extends DurableObject = DurableObject> { idFromName(name: string): DurableObjectId; idFromString(hexId: string): DurableObjectId; newUniqueId(options?: { jurisdiction?: string }): DurableObjectId; get( id: DurableObjectId, options?: { locationHint?: string }, ): DurableObjectStub<T>; } ``` ### AI Binding ```typescript interface Ai { run( model: string, inputs: Record<string, unknown>, options?: { gateway?: { id: string } }, ): Promise<unknown>; } ``` ### Service Binding ```typescript interface Fetcher { fetch(input: RequestInfo, init?: RequestInit): Promise<Response>; // RPC methods are also available when the target worker exports a WorkerEntrypoint } ``` --- ## wrangler.jsonc Configuration Template ```jsonc { "$schema": "./node_modules/wrangler/config-schema.json", // Required "name": "my-worker", "main": "src/index.ts", "compatibility_date": "2025-09-15", "compatibility_flags": ["nodejs_compat"], // Observability (recommended for production) "observability": { "enabled": true, "head_sampling_rate": 1.0, }, // Performance "placement": { "mode": "smart" }, "upload_source_maps": true, // Cron triggers "triggers": { "crons": ["0 */6 * * *"], }, // Static assets (optional) "assets": { "directory": "./public", }, // Bindings "vars": { "ENVIRONMENT": "production", }, "kv_namespaces": [{ "binding": "CACHE", "id": "namespace-id" }], "d1_databases": [ { "binding": "DB", "database_name": "my-db", "database_id": "db-id", "migrations_dir": "migrations", }, ], "r2_buckets": [{ "binding": "BUCKET", "bucket_name": "my-files" }], "durable_objects": { "bindings": [{ "name": "STATE", "class_name": "MyDurableObject" }], }, "queues": { "producers": [{ "binding": "QUEUE", "queue": "my-queue" }], "consumers": [ { "queue": "my-queue", "max_batch_size": 10, "max_batch_timeout": 30, "max_retries": 3, }, ], }, "services": [{ "binding": "AUTH", "service": "auth-worker" }], "ai": { "binding": "AI" }, // Durable Object migrations "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyDurableObject"] }], // Environments "env": { "staging": { "name": "my-worker-staging", "vars": { "ENVIRONMENT": "staging" }, "kv_namespaces": [{ "binding": "CACHE", "id": "staging-namespace-id" }], "d1_databases": [ { "binding": "DB", "database_name": "my-db-staging", "database_id": "staging-db-id", }, ], }, }, } ``` --- ## CPU Time Limits | Plan | Per-Request Limit | Cron Trigger Limit | | ------------------- | ----------------- | ------------------ | | Free | 10 ms CPU time | 15 minutes | | Paid (default) | 30 seconds | 15 minutes | | Paid (configurable) | Up to 15 minutes | 15 minutes | **Note:** CPU time excludes I/O wait. A Worker that makes a 2-second fetch but uses 5ms CPU only consumes 5ms of its limit. --- ## Named Constants Reference ```typescript // Common constants for Workers applications const DEFAULT_PORT = 8_787; const WORKER_MEMORY_LIMIT_MB = 128; const KV_MAX_VALUE_SIZE = 25 * 1024 * 1024; // 25 MB const KV_MAX_KEY_SIZE = 512; // 512 bytes const KV_MAX_LIST_KEYS = 1_000; const R2_MAX_SINGLE_PUT_SIZE = 5 * 1024 * 1024 * 1024; // ~5 GiB (single PUT) const R2_MAX_MULTIPART_SIZE = 5 * 1024 * 1024 * 1024 * 1024; // ~5 TiB (multipart) const D1_MAX_DB_SIZE = 10 * 1024 * 1024 * 1024; // 10 GB const QUEUE_MAX_MESSAGE_SIZE = 128 * 1024; // 128 KB const QUEUE_MAX_BATCH_SIZE = 100; ``` -
SKILL.md 19.9 KB
--- name: infra-platform-cloudflare-workers description: Cloudflare Workers edge compute platform — Wrangler CLI, KV, D1, R2, Durable Objects, Queues, Workers AI --- # Cloudflare Workers Patterns > **Quick Guide:** Cloudflare Workers run TypeScript/JavaScript on Cloudflare's global edge network with V8 isolates (not containers). Use `wrangler.jsonc` for configuration, `wrangler dev` for local development, and `wrangler deploy` for production. Access KV, D1, R2, Queues, Durable Objects, and Workers AI through type-safe bindings on the `env` parameter. Run `wrangler types` to auto-generate your `Env` interface. Stream large payloads — Workers have a 128 MB memory limit. Never store request-scoped state in module-level variables. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST run `wrangler types` to generate your Env interface — NEVER hand-write binding types)** **(You MUST use `wrangler.jsonc` for new projects — Cloudflare recommends JSON config and some features are JSON-only)** **(You MUST stream large request/response bodies — NEVER buffer entire payloads in memory (128 MB limit))** **(You MUST avoid module-level mutable state — Workers reuse V8 isolates across requests, causing cross-request data leaks)** **(You MUST use bindings for Cloudflare services (KV, D1, R2, Queues) — NEVER use REST APIs from within Workers)** </critical_requirements> --- ## Examples - [Core Setup & Configuration](examples/core.md) — wrangler.jsonc, project init, fetch handler, secrets, multi-env, CI/CD, testing - [KV Storage](examples/kv.md) — KV binding, typed get/put, TTL, stale-while-revalidate caching - [D1 Database](examples/d1.md) — D1 binding, parameterized queries, batch ops, migrations, CRUD API - [R2 Object Storage](examples/r2.md) — R2 binding, file upload/download/delete with streaming - [Durable Objects](examples/durable-objects.md) — DO classes, SQLite, RPC, rate limiter, WebSocket chat - [Routing & Middleware](examples/routing.md) — API framework integration, middleware, queues, cron, service bindings, AI, streaming - [Quick Reference](reference.md) — Wrangler CLI commands, binding type signatures, config template, CPU limits --- **Auto-detection:** Cloudflare Workers, wrangler, wrangler.toml, wrangler.jsonc, Workers KV, Cloudflare KV, D1 database, R2 bucket, Durable Objects, Cloudflare Queues, Workers AI, service binding, miniflare, compatibility_date, compatibility_flags, nodejs_compat, cloudflare:workers, ExportedHandler, DurableObject, wrangler dev, wrangler deploy, wrangler types, Cloudflare Pages Functions, edge worker, CF Worker **When to use:** - Deploying TypeScript/JavaScript to Cloudflare's edge network - Configuring Wrangler CLI for local development and deployment - Using Cloudflare bindings: KV, D1, R2, Queues, Durable Objects, Workers AI - Building APIs on Workers with a framework (e.g., Hono) - Implementing real-time features with Durable Objects and WebSockets - Setting up cron triggers and scheduled handlers - Configuring service bindings for worker-to-worker communication - Managing environment variables, secrets, and multi-environment deploys **When NOT to use:** - Long-running compute tasks exceeding CPU time limits (use traditional servers or Workflows) - Applications requiring persistent TCP connections to external databases without Hyperdrive - Workloads needing more than 128 MB memory per request **Key patterns covered:** - Wrangler configuration (`wrangler.jsonc`) and project setup - Fetch handler, scheduled handler, and queue handler - KV key-value storage (caching, config, session data) - D1 SQLite database (relational data at the edge) - R2 S3-compatible object storage (files, uploads, assets) - Durable Objects (stateful edge compute, WebSockets, coordination) - Cloudflare Queues (async message processing) - Workers AI (inference at the edge) - Framework integration (Hono examples) with typed bindings - Environment variables, secrets, and multi-environment config - Service bindings (worker-to-worker RPC) - Cron triggers and scheduled workers - Streaming and performance optimization - Testing with Workers-native test pool --- <philosophy> ## Philosophy Cloudflare Workers run on V8 isolates (not containers) across 300+ data centers worldwide. They start in under 5ms with zero cold starts. The programming model is fundamentally different from traditional servers: 1. **Bindings over APIs** - Access Cloudflare services (KV, D1, R2, Queues) through direct in-process bindings on the `env` parameter, not REST API calls. Bindings have zero network hop and zero auth overhead. 2. **Stateless by default** - Each request gets a fresh execution context. Workers reuse V8 isolates, so module-level variables persist across requests — this is a bug source, not a feature. 3. **Stream everything** - Workers have a 128 MB memory limit. Buffer nothing; stream request and response bodies using `TransformStream` and `pipeTo`. 4. **Edge-first architecture** - Code runs closest to the user. Use Durable Objects when you need coordination or state; use D1/KV/R2 for persistence. **When to use Workers:** - API endpoints, middleware, and request routing - Caching layers and content transformation - Webhook receivers and event processors - Real-time collaboration (with Durable Objects) - Full-stack applications (with Workers Static Assets or Pages) **When NOT to use Workers:** - CPU-intensive compute exceeding limits (10ms free / 30s paid per request) - Workloads requiring more than 128 MB memory - Applications needing persistent database connections (use Hyperdrive as a proxy) - Long-running background jobs exceeding limits (use Workflows for durable execution) </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Project Setup and Wrangler Configuration Every Workers project starts with `wrangler.jsonc`. Cloudflare recommends JSON format — some newer features are JSON-only. Run `wrangler types` after changing bindings to regenerate the `Env` interface. ```jsonc // wrangler.jsonc { "$schema": "./node_modules/wrangler/config-schema.json", "name": "my-api", "main": "src/index.ts", "compatibility_date": "2025-09-15", "compatibility_flags": ["nodejs_compat"], "observability": { "enabled": true }, "placement": { "mode": "smart" }, "upload_source_maps": true, } ``` See [examples/core.md](examples/core.md) for full project initialization, multi-environment config, secrets management, CI/CD, and testing setup. --- ### Pattern 2: Fetch Handler (Request/Response) The fetch handler is the entry point for HTTP requests. Use `satisfies ExportedHandler<Env>` for type safety. ```typescript import type { ExportedHandler } from "cloudflare:workers"; export default { async fetch(request, env, ctx): Promise<Response> { const url = new URL(request.url); if (url.pathname === "/health") { return Response.json({ status: "healthy" }); } return new Response("Not Found", { status: 404 }); }, } satisfies ExportedHandler<Env>; ``` Never store mutable state in module-level variables — V8 isolate reuse causes cross-request data leaks. See [examples/core.md](examples/core.md) for complete handler with CORS and routing. --- ### Pattern 3: KV Key-Value Storage KV is an eventually-consistent key-value store for read-heavy workloads. Use typed `get<T>(key, "json")`, always set `expirationTtl`, and use `ctx.waitUntil()` for non-blocking writes. ```typescript const CACHE_TTL_SECONDS = 3_600; const profile = await env.CACHE.get<UserProfile>(`user:${id}`, "json"); ctx.waitUntil( env.CACHE.put(`user:${id}`, JSON.stringify(data), { expirationTtl: CACHE_TTL_SECONDS, }), ); ``` **When to use:** Read-heavy workloads (config, cache, feature flags) where eventual consistency is acceptable (~60s propagation). **When not to use:** Relational data (use D1), frequent writes to same key, strong consistency (use Durable Objects). See [examples/kv.md](examples/kv.md) for stale-while-revalidate pattern and full caching examples. --- ### Pattern 4: D1 SQLite Database D1 is serverless SQLite at the edge. Always use parameterized queries via `prepare().bind()` to prevent SQL injection. Use `batch()` for atomic multi-statement operations. Use `withSession()` for read replica consistency when read replication is enabled. ```typescript const user = await db .prepare("SELECT * FROM users WHERE email = ?") .bind(email) .first<User>(); const { results } = await db .prepare("SELECT * FROM users LIMIT ? OFFSET ?") .bind(DEFAULT_PAGE_SIZE, offset) .all<User>(); ``` See [examples/d1.md](examples/d1.md) for migrations, batch operations, and full CRUD API. --- ### Pattern 5: R2 Object Storage R2 is S3-compatible storage with zero egress fees. Always stream R2 bodies directly to responses — never call `.arrayBuffer()` or `.text()` on large objects. ```typescript const object = await env.BUCKET.get(key); if (!object) return new Response("Not Found", { status: 404 }); const headers = new Headers(); object.writeHttpMetadata(headers); return new Response(object.body, { headers }); // Stream directly ``` See [examples/r2.md](examples/r2.md) for file service with content-type validation and list operations. --- ### Pattern 6: Durable Objects (Stateful Edge Compute) Durable Objects provide single-threaded, strongly consistent compute. Each instance has SQLite storage. Use RPC methods (not fetch) and `blockConcurrencyWhile` for schema migrations. ```typescript import { DurableObject } from "cloudflare:workers"; export class Counter extends DurableObject<Env> { constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); ctx.blockConcurrencyWhile(async () => { this.ctx.storage.sql.exec("CREATE TABLE IF NOT EXISTS ..."); }); } async increment(name: string): Promise<number> { /* RPC method */ } } ``` **When to use:** Coordination (chat, collaboration), per-entity state (sessions, game instances), WebSocket connections, rate limiting. **When not to use:** Stateless requests, high fan-out, global rate limiting (bottleneck). See [examples/durable-objects.md](examples/durable-objects.md) for rate limiter and WebSocket chat with hibernation. --- ### Pattern 7: Queues, Cron, Service Bindings, Workers AI **Queues** decouple producers from consumers with at-least-once delivery and configurable retries. **Cron triggers** invoke Workers on a schedule. **Service bindings** enable zero-cost worker-to-worker calls. **Workers AI** runs inference on Cloudflare's GPU network. See [examples/routing.md](examples/routing.md) for all these patterns with full configuration and code examples. --- ### Pattern 8: API Framework Integration For structured APIs, use a routing framework with typed bindings. Export the framework's `fetch` handler alongside scheduled/queue handlers. ```typescript import { Hono } from "hono"; const app = new Hono<{ Bindings: Env }>(); app.get("/users/:id", async (c) => { const user = await c.env.DB.prepare("SELECT * FROM users WHERE id = ?") .bind(c.req.param("id")) .first(); return user ? c.json(user) : c.json({ error: "Not found" }, 404); }); export default app; ``` See [examples/routing.md](examples/routing.md) for production API with middleware, error handling, and multi-handler setup. </patterns> --- <performance> ## Performance Optimization ### Request Processing | Technique | Impact | | ------------------------------------- | -------------------------------------------------------- | | Smart Placement (`"mode": "smart"`) | Routes to optimal data center based on binding locations | | Streaming responses | Avoids 128 MB memory limit, improves TTFB | | `ctx.waitUntil()` for background work | Returns response immediately, processes async work after | | `nodejs_compat` flag | Access Node.js built-in modules (crypto, buffer, stream) | ### Storage Performance | Storage | Reads | Writes | Consistency | Best For | | --------------- | ------------------ | ----------------------- | ------------------------ | ------------------------ | | KV | Fast (edge cached) | Slow (~60s propagation) | Eventually consistent | Cache, config, flags | | D1 | Medium | Medium | Strong (per-region) | Relational data, queries | | R2 | Medium | Medium | Strong | Files, blobs, uploads | | Durable Objects | Fast (in-memory) | Fast (SQLite) | Strong (single-threaded) | Coordination, real-time | ### Hyperdrive for External Databases When connecting to PostgreSQL/MySQL outside Cloudflare, always use Hyperdrive. It maintains a connection pool close to your database, eliminating per-request TCP/TLS overhead. ### CPU Time Limits | Plan | CPU Time per Request | | ------------- | --------------------------------------------------- | | Free | 10 ms | | Paid | 30 seconds (default), configurable up to 15 minutes | | Cron Triggers | 15 minutes | </performance> --- <decision_framework> ## Decision Framework ### Choosing a Storage Primitive ``` What kind of data? | +-- Key-value pairs (cache, config, sessions) | +-- Read-heavy, eventual consistency OK --> KV | +-- Strong consistency needed --> Durable Objects | +-- Relational data with queries | +-- Edge-native SQLite --> D1 | +-- External PostgreSQL/MySQL --> Hyperdrive | +-- Files and blobs (images, documents) | +-- S3-compatible storage --> R2 | +-- Coordination state (chat, multiplayer, collaboration) | +-- Single-threaded consistency --> Durable Objects | +-- Message passing / background work +-- Simple fan-out, buffering --> Queues +-- Multi-step durable execution --> Workflows ``` ### Choosing Between Workers and Pages ``` What are you building? | +-- API only (no frontend) --> Workers | +-- Static site + API --> Pages with Functions | +-- Full-stack with SSR --> Pages (framework) or Workers + Static Assets | +-- Background processing / cron --> Workers (Pages lacks cron support) | +-- Real-time / WebSockets --> Workers + Durable Objects ``` ### When to Use Durable Objects vs D1 ``` Do you need coordination between concurrent requests? | +-- YES (chat, game, collaboration) --> Durable Objects | +-- Single-threaded, no race conditions | +-- WebSocket support with hibernation | +-- Per-entity sharding (one DO per room/session) | +-- NO (CRUD, reporting, querying) --> D1 +-- Full SQL support +-- Cross-entity queries +-- Traditional database patterns ``` </decision_framework> --- <integration> ## Integration Guide **Cloudflare Platform Services:** - **Hyperdrive**: Connection pooling proxy for external PostgreSQL/MySQL — eliminates per-request TCP/TLS overhead - **Vectorize**: Vector database for embeddings and semantic search with Workers AI - **Cloudflare Pages**: Static site hosting with Workers-powered functions for full-stack apps - **GitHub Actions**: `cloudflare/wrangler-action@v3` for CI/CD deployment **Framework & ORM Compatibility:** Workers are compatible with edge-optimized frameworks and ORMs that support the V8 runtime. Any framework that exports a `fetch` handler works (Hono, itty-router, etc.). D1 works with any ORM that supports SQLite (check your ORM's Workers compatibility docs). **Testing:** Use `@cloudflare/vitest-pool-workers` to run tests inside the actual Workers runtime with real bindings (KV, D1, R2). See [examples/core.md](examples/core.md) for test configuration. </integration> --- <red_flags> ## RED FLAGS **High Priority Issues:** - Storing request-scoped data in module-level variables (V8 isolate reuse causes cross-request leaks) - Buffering entire request/response bodies with `.text()` / `.arrayBuffer()` (128 MB memory limit) - Hand-writing `Env` interface instead of running `wrangler types` (mismatches between config and code) - Using REST APIs for KV/D1/R2/Queues from within Workers instead of bindings (unnecessary latency and auth overhead) - Putting secrets in `wrangler.jsonc`, source code, or environment variables (use `wrangler secret put`) - Creating a single global Durable Object for all traffic (bottleneck at ~1000 req/sec) **Medium Priority Issues:** - Using `wrangler.toml` for new projects (JSON format recommended, some features JSON-only) - Outdated `compatibility_date` (misses runtime improvements and bug fixes) - Missing `observability` config (production Workers are a black box without logs/traces) - Destructuring `ctx` in fetch handler (loses `this` binding, `ctx.waitUntil` throws "Illegal invocation") - Using `exec()` for D1 queries instead of `prepare().bind()` (no parameterization, SQL injection risk) - Not reciprocating WebSocket close in Durable Objects (causes 1006 errors) **Common Mistakes:** - Forgetting that KV is eventually consistent (~60s propagation) and expecting instant reads after writes - Using `Math.random()` for security-sensitive tokens (use `crypto.randomUUID()` or `crypto.getRandomValues()`) - Not using `ctx.waitUntil()` for post-response background work (work may be cancelled when response is sent) - Comparing secrets with `===` instead of `crypto.subtle.timingSafeEqual()` (timing side-channel attack) - Using `passThroughOnException()` as error handling (hides bugs, use explicit try/catch) - Floating promises (not awaited, not returned, not passed to `waitUntil()`) causing silent failures — enable `@typescript-eslint/no-floating-promises` to catch at dev time **Gotchas and Edge Cases:** - Bindings (KV, D1, R2, etc.) are NOT inherited across Wrangler environments — you must re-declare them per environment - `wrangler dev` uses local simulation by default; use `--remote` to test against real Cloudflare services - D1 batch operations execute sequentially (not in parallel) but atomically - Durable Objects in-memory state is lost on eviction — always persist important data to SQLite first - Unnecessary `await` between DO storage writes breaks write coalescing — batch writes happen atomically when you don't await between them - DO alarm handlers may fire multiple times — design them to be idempotent - Using `blockConcurrencyWhile()` on every request limits throughput to ~200 req/sec — use it only for initialization - Workers on the free plan have a 10ms CPU time limit per request (not wall-clock time — I/O waiting is free) - Cron trigger changes take up to 15 minutes to propagate globally - `.dev.vars` file is for local secrets only and must be gitignored - The `env` parameter is provided per-request by the runtime; avoid caching binding references or derived objects at module scope </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST run `wrangler types` to generate your Env interface — NEVER hand-write binding types)** **(You MUST use `wrangler.jsonc` for new projects — Cloudflare recommends JSON config and some features are JSON-only)** **(You MUST stream large request/response bodies — NEVER buffer entire payloads in memory (128 MB limit))** **(You MUST avoid module-level mutable state — Workers reuse V8 isolates across requests, causing cross-request data leaks)** **(You MUST use bindings for Cloudflare services (KV, D1, R2, Queues) — NEVER use REST APIs from within Workers)** **Failure to follow these rules will result in memory crashes (buffering), data leaks (module state), type mismatches (hand-written Env), unnecessary latency (REST over bindings), and secret exposure (secrets in config).** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.