Claude Skill

infra-platform-cloudflare-workers

Cloudflare Workers edge compute platform — Wrangler CLI, KV, D1, R2, Durable Objects, Queues, Workers AI

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_infra-platform-cloudflare-workers_skills_infra-platform-cloudflare-workers-3a51ef5.zip · 29 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/infra-platform-cloudflare-workers/skills/infra-platform-cloudflare-workers
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git 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.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 — 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 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>

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.

No comments yet.

Reviews (0)

No reviews yet.

Related