Use Cases (Updated: 6/3/2026)

Cloudflare Workers with Claude Code: Practical Edge API Guide

Build a Workers API with Claude Code, Wrangler, KV, D1, R2, caching, rate limits, logs, and security headers.

Cloudflare Workers with Claude Code: Practical Edge API Guide

Cloudflare Workers runs JavaScript and TypeScript on Cloudflare’s edge network. In plain terms, “edge” means your code runs closer to the user instead of only in one central region. That makes Workers a strong fit for small APIs, webhooks, security checks, cached responses, and frontend-specific backend logic.

Claude Code works well here because a Worker is usually a small request handler plus explicit bindings and a wrangler.toml file. I rechecked the official docs on June 3, 2026; keep these references open: Cloudflare Workers, Wrangler, bindings, KV, D1, R2, Cache API, Rate Limiting, and Workers Logs. For adjacent serverless choices, read Claude Code serverless functions and the AWS Lambda guide.

What We Are Building

The example is a small orders API. It stores orders in D1, reads feature flags from KV, writes receipt metadata to R2, caches safe GET responses for a short time, rate-limits authenticated calls, emits structured logs, and attaches security headers to every response.

flowchart LR
  Client["Client"] --> Worker["Worker fetch handler"]
  Worker --> D1["D1 orders"]
  Worker --> KV["KV settings"]
  Worker --> R2["R2 receipts"]
  Worker --> Cache["Cache API"]
  Worker --> Logs["Workers Logs"]

A binding is a named capability injected into your Worker. If wrangler.toml declares binding = "DB", your code uses it as env.DB.

Prompt Claude Code First

Implement a Cloudflare Workers + TypeScript orders API.

Files:
- src/index.ts
- wrangler.toml
- schema.sql

Requirements:
- Use the module fetch handler format.
- GET /health returns JSON.
- GET /orders/:id reads one row from D1 and caches only safe public output for 30 seconds.
- POST /orders validates JSON and inserts into D1.
- Check Authorization Bearer token against API_TOKEN.
- Use SETTINGS KV, DB D1, RECEIPTS R2, and API_RATE_LIMITER bindings.
- Add security headers to every response.
- Log structured JSON objects.
- Include curl verification commands.

Do not:
- Put secret values in wrangler.toml.
- Assume a long-running Node.js server.
- Return pseudocode.

Wrangler Setup

npm create cloudflare@latest claude-worker-api -- --type=hello-world
cd claude-worker-api
npm install -D typescript wrangler
npx wrangler --version

C3 may generate wrangler.jsonc in new projects. Wrangler supports JSON/JSONC and TOML configuration files; this article uses TOML for readability. If your project already has wrangler.jsonc, keep the same keys in JSONC form.

name = "claude-worker-api"
main = "src/index.ts"
compatibility_date = "2026-06-03"

[vars]
PUBLIC_ENV = "production"

[observability]
enabled = true
head_sampling_rate = 1

[[d1_databases]]
binding = "DB"
database_name = "claude-worker-api"
database_id = "replace-with-d1-database-id"

[[kv_namespaces]]
binding = "SETTINGS"
id = "replace-with-kv-namespace-id"

[[r2_buckets]]
binding = "RECEIPTS"
bucket_name = "claude-worker-receipts"

[[ratelimits]]
name = "API_RATE_LIMITER"
namespace_id = "1001"

  [ratelimits.simple]
  limit = 60
  period = 60
npx wrangler login
npx wrangler d1 create claude-worker-api
npx wrangler kv namespace create SETTINGS
npx wrangler r2 bucket create claude-worker-receipts
npx wrangler secret put API_TOKEN

D1 Schema

CREATE TABLE IF NOT EXISTS orders (
  id TEXT PRIMARY KEY,
  email TEXT NOT NULL,
  amount INTEGER NOT NULL,
  status TEXT NOT NULL DEFAULT 'pending',
  created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS idx_orders_email ON orders(email);
npx wrangler d1 execute claude-worker-api --local --file=./schema.sql
npx wrangler d1 execute claude-worker-api --remote --file=./schema.sql

Runnable Worker Handler

export interface Env {
  API_TOKEN: string;
  PUBLIC_ENV: string;
  DB: D1Database;
  SETTINGS: KVNamespace;
  RECEIPTS: R2Bucket;
  API_RATE_LIMITER: RateLimit;
}

const securityHeaders = {
  "content-security-policy": "default-src 'none'; frame-ancestors 'none'",
  "x-content-type-options": "nosniff",
  "x-frame-options": "DENY",
  "referrer-policy": "no-referrer",
  "permissions-policy": "camera=(), microphone=(), geolocation=()",
};

function json(data: unknown, init: ResponseInit = {}) {
  return new Response(JSON.stringify(data), {
    ...init,
    headers: {
      "content-type": "application/json; charset=utf-8",
      ...securityHeaders,
      ...init.headers,
    },
  });
}

export default {
  async fetch(request, env, ctx): Promise<Response> {
    const url = new URL(request.url);
    const requestId = crypto.randomUUID();

    console.log({ event: "request_started", requestId, method: request.method, path: url.pathname });

    if (url.pathname === "/health") {
      const maintenance = await env.SETTINGS.get("maintenance");
      return json({ ok: true, env: env.PUBLIC_ENV, maintenance: maintenance === "true" });
    }

    if (request.headers.get("authorization") !== `Bearer ${env.API_TOKEN}`) {
      return json({ error: "unauthorized" }, { status: 401 });
    }

    const { success } = await env.API_RATE_LIMITER.limit({
      key: request.headers.get("authorization")?.slice(-16) ?? "anonymous",
    });

    if (!success) return json({ error: "rate_limited" }, { status: 429 });

    const match = url.pathname.match(/^\/orders\/([a-zA-Z0-9_-]+)$/);

    if (request.method === "GET" && match) {
      const cache = caches.default;
      const cacheKey = new Request(url.toString(), { method: "GET" });
      const cached = await cache.match(cacheKey);
      if (cached) return cached;

      const order = await env.DB.prepare(
        "SELECT id, email, amount, status, created_at FROM orders WHERE id = ?"
      ).bind(match[1]).first();

      if (!order) return json({ error: "not_found" }, { status: 404 });

      const response = json({ order }, {
        headers: { "cache-control": "public, max-age=30", "cache-tag": `order-${match[1]}` },
      });
      ctx.waitUntil(cache.put(cacheKey, response.clone()));
      return response;
    }

    if (request.method === "POST" && url.pathname === "/orders") {
      const body = await request.json<{ email?: string; amount?: number }>();
      if (!body.email?.includes("@") || !Number.isInteger(body.amount) || body.amount <= 0) {
        return json({ error: "invalid_order" }, { status: 400 });
      }

      const id = crypto.randomUUID();
      await env.DB.prepare(
        "INSERT INTO orders (id, email, amount, status) VALUES (?, ?, ?, ?)"
      ).bind(id, body.email, body.amount, "pending").run();

      await env.RECEIPTS.put(`orders/${id}.json`, JSON.stringify({ id, email: body.email, amount: body.amount }), {
        httpMetadata: { contentType: "application/json" },
      });

      console.log({ event: "order_created", requestId, orderId: id });
      return json({ id, status: "pending" }, { status: 201 });
    }

    return json({ error: "not_found" }, { status: 404 });
  },
} satisfies ExportedHandler<Env>;

Local Test and Deploy

printf "API_TOKEN=dev-token\n" > .dev.vars
npx wrangler dev
curl http://localhost:8787/health

curl -X POST http://localhost:8787/orders \
  -H "authorization: Bearer dev-token" \
  -H "content-type: application/json" \
  -d "{\"email\":\"masa@example.com\",\"amount\":1200}"

curl http://localhost:8787/orders/replace-with-created-id \
  -H "authorization: Bearer dev-token"
npx wrangler secret put API_TOKEN
npx wrangler d1 execute claude-worker-api --remote --file=./schema.sql
npx wrangler deploy
npx wrangler tail

Use Cases

Use case one is a form or order API: D1 stores the record, R2 stores receipt files, and Workers Logs keeps operational evidence.

Use case two is a webhook receiver. Verify signatures, store event IDs in D1, and reject duplicate events.

Use case three is a BFF, or Backend for Frontend. The Worker hides upstream API keys, reshapes data for the UI, and caches safe responses.

Use case four is controlled file delivery from R2. Keep authorization in the Worker and store the heavy object in R2.

These examples stay intentionally small because Workers is strongest when the request boundary is clear. A contact form can be accepted, validated, logged, and stored without booting a full application server. A webhook can return quickly after saving the event, then hand slow work to another service if needed. A BFF can protect a paid upstream API key while still giving the frontend a simple response shape. That is the practical split to ask Claude Code for: keep the edge path fast, explicit, and easy to verify with curl.

For heavier work, draw the boundary early. Image conversion, long PDF generation, model inference with large dependencies, and multi-step jobs may still start at a Worker, but the Worker should enqueue, authorize, or route the task rather than doing all processing inline. Ask Claude Code to explain the handoff before it writes code.

Pitfalls

Do not write Workers like a long-running Node.js server. Prefer Web APIs, fetch, Request, Response, crypto, and bindings.

Do not mix storage roles. KV is for small key-value data, D1 is relational data, R2 is object storage, and Cache API is for short-lived response acceleration.

Do not cache private responses. If output depends on the user, token, cookie, or email address, either avoid shared cache or build a very deliberate key and TTL.

Do not rate-limit only by IP. Use an API key, user ID, tenant ID, or another stable actor identifier when possible.

Also watch the environment split. .dev.vars is useful for local secrets, but production secrets must be registered with wrangler secret put. vars in wrangler.toml are configuration values, not a secret store. When Claude Code edits config, ask it to list which values are public, which are secret, and which ones must be created in the Cloudflare dashboard or with Wrangler.

Finally, review logs as production data. Structured logs are useful because Workers Logs can index fields, but that also means accidental emails, tokens, and request bodies become searchable operational data. Log IDs, status, route names, and coarse error classes. Do not log Authorization headers, cookies, raw webhook payloads, or full customer messages.

Workers vs Pages Functions vs Cloud Run vs Lambda

Choose Workers for low-latency HTTP APIs close to Cloudflare cache and bindings. Choose Pages Functions when a Cloudflare Pages site needs a little dynamic logic next to static assets. Choose Cloud Run when you need containers, long processing, or an existing full framework. Choose Lambda when the workflow is centered on AWS services such as S3, DynamoDB, EventBridge, or SQS.

Claude Code Review Prompt

Review this Cloudflare Workers implementation.

Check:
- Worker runtime compatibility
- Env type and wrangler binding name alignment
- secret leakage in code, logs, and config
- D1 bind usage and SQL injection risk
- unsafe Cache API usage
- Rate Limiting key choice
- security headers on every response
- missing curl verification steps

Return findings by severity, then fixes, then tests.

CTA and Verification Note

Start by migrating one endpoint: /health, a read-only GET, or a webhook receiver. Give Claude Code the exact files, binding names, verification commands, and forbidden shortcuts. For reusable implementation templates, see products; for team adoption and review habits, see training.

Results from actually trying it: I connected the examples in this article as a full Wrangler/D1/Worker flow; before production, re-run wrangler deploy, wrangler tail, header checks, cache behavior, and 429 behavior in your own Cloudflare account.

#Claude Code #Cloudflare Workers #edge computing #serverless #API
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.