7.4 KiB
7.4 KiB
Cloudflare Workers Best Practices
High-level guidance for Workers that invoke Durable Objects.
Wrangler Configuration
wrangler.jsonc (Recommended)
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2024-12-01",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [
{ "name": "CHAT_ROOM", "class_name": "ChatRoom" },
{ "name": "USER_SESSION", "class_name": "UserSession" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["ChatRoom", "UserSession"] }
],
// Environment variables
"vars": {
"ENVIRONMENT": "production"
},
// KV namespaces
"kv_namespaces": [
{ "binding": "CONFIG", "id": "abc123" }
],
// R2 buckets
"r2_buckets": [
{ "binding": "UPLOADS", "bucket_name": "my-uploads" }
],
// D1 databases
"d1_databases": [
{ "binding": "DB", "database_id": "xyz789" }
]
}
wrangler.toml (Alternative)
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"]
[[durable_objects.bindings]]
name = "CHAT_ROOM"
class_name = "ChatRoom"
[[migrations]]
tag = "v1"
new_sqlite_classes = ["ChatRoom"]
[vars]
ENVIRONMENT = "production"
TypeScript Types
Environment Interface
// src/types.ts
import { ChatRoom } from "./durable-objects/chat-room";
import { UserSession } from "./durable-objects/user-session";
export interface Env {
// Durable Objects
CHAT_ROOM: DurableObjectNamespace<ChatRoom>;
USER_SESSION: DurableObjectNamespace<UserSession>;
// KV
CONFIG: KVNamespace;
// R2
UPLOADS: R2Bucket;
// D1
DB: D1Database;
// Environment variables
ENVIRONMENT: string;
API_KEY: string; // From secrets
}
Export Durable Object Classes
// src/index.ts
export { ChatRoom } from "./durable-objects/chat-room";
export { UserSession } from "./durable-objects/user-session";
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// Worker handler
},
};
Worker Handler Pattern
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url);
try {
// Route to appropriate handler
if (url.pathname.startsWith("/api/rooms")) {
return handleRooms(request, env);
}
if (url.pathname.startsWith("/api/users")) {
return handleUsers(request, env);
}
return new Response("Not Found", { status: 404 });
} catch (error) {
console.error("Request failed:", error);
return new Response("Internal Server Error", { status: 500 });
}
},
};
async function handleRooms(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const roomId = url.searchParams.get("room");
if (!roomId) {
return Response.json({ error: "Missing room parameter" }, { status: 400 });
}
const stub = env.CHAT_ROOM.getByName(roomId);
if (request.method === "POST") {
const body = await request.json<{ userId: string; message: string }>();
const result = await stub.sendMessage(body.userId, body.message);
return Response.json(result);
}
const messages = await stub.getMessages();
return Response.json(messages);
}
Request Validation
import { z } from "zod";
const SendMessageSchema = z.object({
userId: z.string().min(1),
message: z.string().min(1).max(1000),
});
async function handleSendMessage(request: Request, env: Env): Promise<Response> {
const body = await request.json();
const result = SendMessageSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ error: "Validation failed", details: result.error.issues },
{ status: 400 }
);
}
const stub = env.CHAT_ROOM.getByName(result.data.userId);
const message = await stub.sendMessage(result.data.userId, result.data.message);
return Response.json(message);
}
Observability & Logging
Structured Logging
function log(level: "info" | "warn" | "error", message: string, data?: Record<string, unknown>) {
console.log(JSON.stringify({
level,
message,
timestamp: new Date().toISOString(),
...data,
}));
}
// Usage
log("info", "Request received", { path: url.pathname, method: request.method });
log("error", "DO call failed", { roomId, error: String(error) });
Request Tracing
async function handleRequest(request: Request, env: Env): Promise<Response> {
const requestId = crypto.randomUUID();
const startTime = Date.now();
try {
const response = await processRequest(request, env);
log("info", "Request completed", {
requestId,
duration: Date.now() - startTime,
status: response.status,
});
return response;
} catch (error) {
log("error", "Request failed", {
requestId,
duration: Date.now() - startTime,
error: String(error),
});
throw error;
}
}
Tail Workers (Production)
For production logging, use Tail Workers to forward logs:
// wrangler.jsonc
{
"tail_consumers": [
{ "service": "log-collector" }
]
}
Error Handling
Graceful DO Errors
async function callDO(stub: DurableObjectStub<ChatRoom>, method: string): Promise<Response> {
try {
const result = await stub.getMessages();
return Response.json(result);
} catch (error) {
if (error instanceof Error) {
// DO threw an error
log("error", "DO operation failed", { error: error.message });
return Response.json(
{ error: "Service temporarily unavailable" },
{ status: 503 }
);
}
throw error;
}
}
Timeout Handling
async function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), ms)
);
return Promise.race([promise, timeout]);
}
// Usage
const result = await withTimeout(stub.processData(data), 5000);
CORS Handling
function corsHeaders(): HeadersInit {
return {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
};
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders() });
}
const response = await handleRequest(request, env);
// Add CORS headers to response
const newHeaders = new Headers(response.headers);
Object.entries(corsHeaders()).forEach(([k, v]) => newHeaders.set(k, v));
return new Response(response.body, {
status: response.status,
headers: newHeaders,
});
},
};
Secrets Management
Set secrets via wrangler CLI (not in config files):
wrangler secret put API_KEY
wrangler secret put DATABASE_URL
Access in code:
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const apiKey = env.API_KEY; // From secret
// ...
},
};
Development Commands
# Local development
wrangler dev
# Deploy
wrangler deploy
# Tail logs
wrangler tail
# List DOs
wrangler d1 execute DB --command "SELECT * FROM _cf_DO"