Files
blogflare/.agents/plugins/cloudflare/skills/durable-objects/references/workers.md
T
2026-09-25 12:11:51 +08:00

7.1 KiB

Cloudflare Workers Best Practices

High-level guidance for Workers that invoke Durable Objects.

Wrangler Configuration

{
	"$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"