Cloudflare Workers runs JavaScript at the edge — in data centers close to users worldwide, not in a central region. For the right workloads (API routing, authentication, caching, data transformation), Workers eliminate the cold start latency of traditional serverless and give sub-millisecond response times. Claude Code understands the Workers runtime constraints and generates code that works within the 50ms CPU time limit and the Web API-based environment.
This guide covers Cloudflare Workers with Claude Code: routing with Hono, KV for caching, D1 for relational data, R2 for files, and Durable Objects for stateful edge.
Workers Setup
CLAUDE.md for Cloudflare Workers Projects
## Cloudflare Workers Project
- Runtime: Workers (not Node.js) — uses Web APIs (fetch, Response, Request, crypto.subtle)
- Framework: Hono (lightweight, fast, designed for edge)
- Database: Cloudflare D1 (SQLite) via Drizzle ORM
- Cache: Cloudflare KV for session data and cache
- Storage: R2 for user-uploaded files
- Local dev: `wrangler dev` (runs Workers runtime locally)
## What's NOT available in Workers
- No Node.js built-ins (no fs, no path, no os, no child_process)
- No Buffer — use Uint8Array and TextEncoder/TextDecoder
- No import of Node.js modules unless they have a browser-compatible build
- Limited CPU time: 50ms per request (exceptions: Durable Objects, paid plan 30s)
## Wrangler conventions
- wrangler.toml: declares KV namespaces, D1 databases, R2 buckets, bindings
- Env type: Bindings are in the `Env` interface, accessed via `c.env.KV_NAMESPACE`
- TypeScript: generate types with `wrangler types`
- Testing: `wrangler dev --local` for local dev, vitest for unit tests
Hono Application Structure
// src/index.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { logger } from 'hono/logger';
import { bearerAuth } from 'hono/bearer-auth';
import { drizzle } from 'drizzle-orm/d1';
import { usersRouter } from './routes/users';
import { productsRouter } from './routes/products';
import type { Env } from './types';
const app = new Hono<{ Bindings: Env }>();
// Middleware
app.use('*', logger());
app.use('*', cors({
origin: (origin) => origin, // Echo origin (allows any — configure for production)
credentials: true,
}));
// Protected routes
app.use('/api/*', bearerAuth({ token: '' })); // Custom token verification below
// Custom JWT verification middleware
app.use('/api/*', async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '');
if (!token) return c.json({ error: 'Unauthorized' }, 401);
// Web Crypto API — available in Workers
const payload = await verifyJWT(token, c.env.JWT_SECRET);
if (!payload) return c.json({ error: 'Invalid token' }, 401);
c.set('userId', payload.sub);
await next();
});
app.route('/api/users', usersRouter);
app.route('/api/products', productsRouter);
app.get('/health', (c) => c.json({ ok: true, region: c.req.header('CF-Ray') }));
export default app;
# wrangler.toml
name = "my-api"
main = "src/index.ts"
compatibility_date = "2024-10-01"
node_compat = false # We don't need Node compatibility
[[kv_namespaces]]
binding = "SESSIONS"
id = "your-kv-namespace-id"
preview_id = "your-preview-kv-id"
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "your-database-id"
[[r2_buckets]]
binding = "FILES"
bucket_name = "my-files"
[vars]
ENVIRONMENT = "production"
[secrets]
# Set with: wrangler secret put JWT_SECRET
JWT_SECRET = ""
D1 Database with Drizzle
Build the users table and CRUD API using D1.
// src/db/schema.ts
import { sqliteTable, text, integer, index } from 'drizzle-orm/sqlite-core';
export const users = sqliteTable('users', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
email: text('email').notNull().unique(),
name: text('name').notNull(),
passwordHash: text('password_hash').notNull(),
createdAt: integer('created_at', { mode: 'timestamp' })
.$defaultFn(() => new Date()),
updatedAt: integer('updated_at', { mode: 'timestamp' })
.$defaultFn(() => new Date()),
}, (table) => ({
emailIdx: index('users_email_idx').on(table.email),
}));
// src/routes/users.ts
import { Hono } from 'hono';
import { drizzle } from 'drizzle-orm/d1';
import { users } from '../db/schema';
import { eq } from 'drizzle-orm';
import { hashPassword, verifyPassword } from '../lib/crypto';
import type { Env } from '../types';
const router = new Hono<{ Bindings: Env; Variables: { userId: string } }>();
router.get('/:id', async (c) => {
const db = drizzle(c.env.DB, { schema: { users } });
const userId = c.req.param('id');
// D1 allows only current user to see their profile
if (c.get('userId') !== userId) {
return c.json({ error: 'Forbidden' }, 403);
}
const [user] = await db.select({
id: users.id,
email: users.email,
name: users.name,
createdAt: users.createdAt,
}).from(users).where(eq(users.id, userId));
if (!user) return c.json({ error: 'Not found' }, 404);
return c.json({ user });
});
router.post('/', async (c) => {
const db = drizzle(c.env.DB, { schema: { users } });
const body = await c.req.json();
// Workers has Web Crypto for hashing — no bcrypt (it's a Node.js package)
const passwordHash = await hashPassword(body.password);
try {
const [user] = await db.insert(users)
.values({ email: body.email, name: body.name, passwordHash })
.returning({ id: users.id, email: users.email, name: users.name });
return c.json({ user }, 201);
} catch (error: any) {
if (error.message?.includes('UNIQUE constraint')) {
return c.json({ error: 'Email already exists' }, 409);
}
throw error;
}
});
export { router as usersRouter };
KV for Caching and Sessions
Cache expensive API responses in KV.
Sessions in KV — not JWT — for instant revocation.
// src/lib/cache.ts
export class KVCache {
constructor(private readonly kv: KVNamespace) {}
async get<T>(key: string): Promise<T | null> {
const value = await this.kv.get(key, 'json');
return value as T | null;
}
async set<T>(key: string, value: T, ttlSeconds: number): Promise<void> {
await this.kv.put(key, JSON.stringify(value), { expirationTtl: ttlSeconds });
}
async del(key: string): Promise<void> {
await this.kv.delete(key);
}
}
// Session management
export async function createSession(
kv: KVNamespace,
userId: string,
): Promise<string> {
const sessionId = crypto.randomUUID();
await kv.put(
`session:${sessionId}`,
JSON.stringify({ userId, createdAt: Date.now() }),
{ expirationTtl: 7 * 24 * 60 * 60 } // 7 days
);
return sessionId;
}
export async function getSession(
kv: KVNamespace,
sessionId: string,
): Promise<{ userId: string } | null> {
return kv.get(`session:${sessionId}`, 'json');
}
export async function deleteSession(kv: KVNamespace, sessionId: string): Promise<void> {
await kv.delete(`session:${sessionId}`);
}
R2 File Storage
Handle file uploads — store in R2, generate signed download URLs.
// src/routes/files.ts
router.post('/upload', async (c) => {
const userId = c.get('userId');
const formData = await c.req.formData();
const file = formData.get('file') as File;
if (!file) return c.json({ error: 'No file provided' }, 400);
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'];
if (!allowedTypes.includes(file.type)) {
return c.json({ error: 'File type not allowed' }, 400);
}
// Validate size (10MB max)
if (file.size > 10 * 1024 * 1024) {
return c.json({ error: 'File too large (max 10MB)' }, 400);
}
const key = `${userId}/${crypto.randomUUID()}-${file.name}`;
await c.env.FILES.put(key, file.stream(), {
httpMetadata: {
contentType: file.type,
},
customMetadata: {
userId,
originalName: file.name,
uploadedAt: new Date().toISOString(),
},
});
// Return the key — client uses a separate endpoint to get a signed URL
return c.json({ key }, 201);
});
router.get('/download/:key{.+}', async (c) => {
const key = c.req.param('key');
const userId = c.get('userId');
const object = await c.env.FILES.get(key);
if (!object) return c.json({ error: 'Not found' }, 404);
// Verify ownership
if (object.customMetadata?.userId !== userId) {
return c.json({ error: 'Forbidden' }, 403);
}
return new Response(object.body, {
headers: {
'Content-Type': object.httpMetadata?.contentType ?? 'application/octet-stream',
'Content-Disposition': `attachment; filename="${object.customMetadata?.originalName}"`,
},
});
});
For deploying SvelteKit and Astro to Cloudflare Pages (not Workers), see the SvelteKit guide and Astro guide. For Durable Objects — the stateful continuation of Workers — Claude Code generates the class-based pattern for collaborative features. The Claude Skills 360 bundle includes Cloudflare Workers skill sets for edge API patterns. Start with the free tier to generate a Workers API with D1 and KV.