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 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.
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.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Obsidian to CLAUDE.md Workflow: Stop Re-explaining Context
Turn Obsidian working notes into concise CLAUDE.md operating notes that make Claude Code sessions easier to resume.
Claude Code Revenue CTA Routing: Send Articles to PDF, Gumroad, and Consultation
A Claude Code workflow for routing article readers to the free PDF, Gumroad products, or consultation by intent.
Claude Code Team Handoff Rules: Review Evidence, Permissions, Rollback, and Revenue Paths
A practical Claude Code handoff format for team review, proof, permission rules, rollback, free PDF, Gumroad, and consultation paths.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.