# Development Best Practices Patterns and best practices for building robust, maintainable Workers applications. ## Testing ### Vitest Integration Workers has first-class Vitest integration for unit and integration testing. **Setup:** ```bash npm install -D vitest @cloudflare/vitest-pool-workers ``` **vitest.config.ts:** ```typescript import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; export default defineWorkersConfig({ test: { poolOptions: { workers: { wrangler: { configPath: "./wrangler.toml" }, }, }, }, }); ``` **Basic Test:** ```typescript import { env, createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; import { describe, it, expect } from "vitest"; import worker from "./index"; describe("Worker", () => { it("responds with JSON", async () => { const request = new Request("http://example.com/api/users"); const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); await waitOnExecutionContext(ctx); expect(response.status).toBe(200); const data = await response.json(); expect(data).toHaveProperty("users"); }); it("handles errors gracefully", async () => { const request = new Request("http://example.com/api/error"); const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); expect(response.status).toBe(500); }); }); ``` **Testing with Bindings:** ```typescript import { env } from "cloudflare:test"; describe("KV operations", () => { it("reads and writes to KV", async () => { // env provides access to bindings configured in wrangler.toml await env.MY_KV.put("test-key", "test-value"); const value = await env.MY_KV.get("test-key"); expect(value).toBe("test-value"); }); }); ``` **Testing Durable Objects:** ```typescript import { env, runInDurableObject } from "cloudflare:test"; describe("Counter Durable Object", () => { it("increments counter", async () => { await runInDurableObject(env.COUNTER, async (instance, state) => { const request = new Request("http://do/increment"); const response = await instance.fetch(request); const data = await response.json(); expect(data.count).toBe(1); }); }); }); ``` **Run Tests:** ```bash npm test # or npx vitest ``` ### Integration Testing Test full request/response cycles with external services. ```typescript describe("External API integration", () => { it("fetches data from external API", async () => { const request = new Request("http://example.com/api/external"); const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); expect(response.status).toBe(200); // Verify external API was called correctly const data = await response.json(); expect(data).toHaveProperty("externalData"); }); }); ``` ### Mocking Mock external dependencies for isolated tests. ```typescript import { vi } from "vitest"; describe("Mocked fetch", () => { it("handles fetch errors", async () => { // Mock global fetch global.fetch = vi.fn().mockRejectedValue(new Error("Network error")); const request = new Request("http://example.com/api/data"); const ctx = createExecutionContext(); const response = await worker.fetch(request, env, ctx); expect(response.status).toBe(503); }); }); ``` ## Error Handling ### Global Error Handling Catch all errors and return appropriate responses. ```typescript export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { try { return await handleRequest(request, env, ctx); } catch (error) { console.error("Uncaught error:", error); return Response.json( { error: "Internal server error", message: error instanceof Error ? error.message : "Unknown error", }, { status: 500 } ); } }, }; async function handleRequest(request: Request, env: Env, ctx: ExecutionContext): Promise { // Your handler logic } ``` ### Custom Error Classes Define custom error types for better error handling. ```typescript class NotFoundError extends Error { constructor(message: string) { super(message); this.name = "NotFoundError"; } } class UnauthorizedError extends Error { constructor(message: string) { super(message); this.name = "UnauthorizedError"; } } class ValidationError extends Error { constructor(public fields: Record) { super("Validation failed"); this.name = "ValidationError"; } } async function handleRequest(request: Request, env: Env): Promise { try { // Your logic const user = await getUser(userId); if (!user) { throw new NotFoundError("User not found"); } return Response.json(user); } catch (error) { if (error instanceof NotFoundError) { return Response.json({ error: error.message }, { status: 404 }); } if (error instanceof UnauthorizedError) { return Response.json({ error: error.message }, { status: 401 }); } if (error instanceof ValidationError) { return Response.json( { error: "Validation failed", fields: error.fields }, { status: 400 } ); } // Unknown error console.error("Unexpected error:", error); return Response.json({ error: "Internal server error" }, { status: 500 }); } } ``` ### Retry Logic Implement retry logic for transient failures. ```typescript async function fetchWithRetry( url: string, options: RequestInit = {}, maxRetries = 3 ): Promise { let lastError: Error; for (let i = 0; i < maxRetries; i++) { try { const response = await fetch(url, options); if (response.ok) { return response; } // Don't retry on client errors (4xx) if (response.status >= 400 && response.status < 500) { return response; } lastError = new Error(`HTTP ${response.status}`); } catch (error) { lastError = error as Error; } // Exponential backoff if (i < maxRetries - 1) { await new Promise((resolve) => setTimeout(resolve, Math.pow(2, i) * 1000)); } } throw lastError!; } ``` ## Performance Optimization ### Caching Strategies Use the Cache API effectively. ```typescript async function handleCachedRequest(request: Request): Promise { const cache = caches.default; const cacheKey = new Request(request.url, request); // Try to get from cache let response = await cache.match(cacheKey); if (!response) { // Cache miss - fetch from origin response = await fetchFromOrigin(request); // Cache successful responses if (response.ok) { response = new Response(response.body, { ...response, headers: { ...Object.fromEntries(response.headers), "Cache-Control": "public, max-age=3600", }, }); // Don't await - cache in background ctx.waitUntil(cache.put(cacheKey, response.clone())); } } return response; } ``` **Cache with custom keys:** ```typescript function getCacheKey(request: Request, userId?: string): Request { const url = new URL(request.url); // Include user ID in cache key for personalized content if (userId) { url.searchParams.set("userId", userId); } return new Request(url.toString(), request); } ``` ### Response Streaming Stream responses for large data. ```typescript export default { async fetch(request: Request): Promise { const { readable, writable } = new TransformStream(); const writer = writable.getWriter(); // Stream data in background (async () => { try { const data = await fetchLargeDataset(); for (const item of data) { await writer.write(new TextEncoder().encode(JSON.stringify(item) + "\n")); } await writer.close(); } catch (error) { await writer.abort(error); } })(); return new Response(readable, { headers: { "Content-Type": "application/x-ndjson", "Transfer-Encoding": "chunked", }, }); }, }; ``` ### Batching Operations Batch multiple operations for better performance. ```typescript // Bad: Sequential operations for (const userId of userIds) { await env.KV.get(`user:${userId}`); } // Good: Batch operations const users = await Promise.all( userIds.map((id) => env.KV.get(`user:${id}`)) ); // Even better: Use batch APIs when available const results = await env.DB.batch([ env.DB.prepare("SELECT * FROM users WHERE id = ?").bind(1), env.DB.prepare("SELECT * FROM users WHERE id = ?").bind(2), env.DB.prepare("SELECT * FROM users WHERE id = ?").bind(3), ]); ``` ### Background Tasks Use `ctx.waitUntil()` for non-critical work. ```typescript export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { // Process request const response = await handleRequest(request, env); // Log analytics in background (don't block response) ctx.waitUntil( env.ANALYTICS.writeDataPoint({ blobs: [request.url, request.method], doubles: [performance.now()], }) ); // Update cache in background ctx.waitUntil(updateCache(request, response, env)); return response; }, }; ``` ## Debugging ### Console Logging Use console methods for debugging. ```typescript console.log("Info:", { userId, action }); console.error("Error occurred:", error); console.warn("Deprecated API used"); console.debug("Debug info:", data); // Structured logging console.log(JSON.stringify({ level: "info", timestamp: Date.now(), userId, action, })); ``` ### Wrangler Tail View real-time logs during development. ```bash # Tail logs wrangler tail # Filter by status wrangler tail --status error # Filter by method wrangler tail --method POST # Pretty format wrangler tail --format pretty ``` ### Source Maps Enable source maps for better error traces. **tsconfig.json:** ```json { "compilerOptions": { "sourceMap": true } } ``` **wrangler.toml:** ```toml upload_source_maps = true ``` ### Local Debugging Use DevTools for debugging. ```bash # Start with inspector wrangler dev --inspector ``` Then open `chrome://inspect` in Chrome and connect to the worker. ### Breakpoints Set breakpoints in your code. ```typescript export default { async fetch(request: Request): Promise { debugger; // Execution will pause here const data = await fetchData(); return Response.json(data); }, }; ``` ## Code Organization ### Router Pattern Organize routes cleanly. ```typescript interface Route { pattern: URLPattern; handler: (request: Request, env: Env, params: URLPatternResult) => Promise; } const routes: Route[] = [ { pattern: new URLPattern({ pathname: "/api/users" }), handler: handleGetUsers, }, { pattern: new URLPattern({ pathname: "/api/users/:id" }), handler: handleGetUser, }, { pattern: new URLPattern({ pathname: "/api/users" }), handler: handleCreateUser, }, ]; export default { async fetch(request: Request, env: Env): Promise { for (const route of routes) { const match = route.pattern.exec(request.url); if (match) { return route.handler(request, env, match); } } return Response.json({ error: "Not found" }, { status: 404 }); }, }; async function handleGetUsers(request: Request, env: Env): Promise { const users = await env.DB.prepare("SELECT * FROM users").all(); return Response.json(users.results); } ``` ### Middleware Pattern Chain middleware for cross-cutting concerns. ```typescript type Middleware = ( request: Request, env: Env, next: () => Promise ) => Promise; const corsMiddleware: Middleware = async (request, env, next) => { if (request.method === "OPTIONS") { return new Response(null, { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE", }, }); } const response = await next(); response.headers.set("Access-Control-Allow-Origin", "*"); return response; }; const authMiddleware: Middleware = async (request, env, next) => { const token = request.headers.get("Authorization"); if (!token) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } // Validate token const isValid = await validateToken(token, env); if (!isValid) { return Response.json({ error: "Invalid token" }, { status: 401 }); } return next(); }; const loggingMiddleware: Middleware = async (request, env, next) => { const start = Date.now(); const response = await next(); const duration = Date.now() - start; console.log({ method: request.method, url: request.url, status: response.status, duration, }); return response; }; function applyMiddleware( handler: (request: Request, env: Env) => Promise, middlewares: Middleware[] ): (request: Request, env: Env) => Promise { return (request: Request, env: Env) => { let index = -1; const dispatch = async (i: number): Promise => { if (i <= index) { throw new Error("next() called multiple times"); } index = i; if (i === middlewares.length) { return handler(request, env); } const middleware = middlewares[i]; return middleware(request, env, () => dispatch(i + 1)); }; return dispatch(0); }; } // Usage const handler = applyMiddleware( async (request, env) => { return Response.json({ message: "Hello!" }); }, [loggingMiddleware, corsMiddleware, authMiddleware] ); export default { fetch: handler }; ``` ### Dependency Injection Use environment for dependencies. ```typescript interface Env { DB: D1Database; CACHE: KVNamespace; } class UserService { constructor(private env: Env) {} async getUser(id: string) { // Try cache first const cached = await this.env.CACHE.get(`user:${id}`); if (cached) return JSON.parse(cached); // Fetch from database const user = await this.env.DB.prepare( "SELECT * FROM users WHERE id = ?" ).bind(id).first(); // Update cache if (user) { await this.env.CACHE.put(`user:${id}`, JSON.stringify(user), { expirationTtl: 3600, }); } return user; } } export default { async fetch(request: Request, env: Env): Promise { const userService = new UserService(env); const user = await userService.getUser("123"); return Response.json(user); }, }; ``` ## Security Best Practices ### Input Validation Always validate and sanitize user input. ```typescript import { z } from "zod"; const userSchema = z.object({ name: z.string().min(1).max(100), email: z.string().email(), age: z.number().int().positive().max(150), }); export default { async fetch(request: Request, env: Env): Promise { try { const body = await request.json(); const validated = userSchema.parse(body); // Use validated data await createUser(validated, env); return Response.json({ success: true }); } catch (error) { if (error instanceof z.ZodError) { return Response.json( { error: "Validation failed", issues: error.errors }, { status: 400 } ); } throw error; } }, }; ``` ### Rate Limiting Implement rate limiting to prevent abuse. ```typescript async function checkRateLimit( identifier: string, env: Env, limit = 100, window = 60 ): Promise { const key = `ratelimit:${identifier}`; const current = await env.CACHE.get(key); if (!current) { await env.CACHE.put(key, "1", { expirationTtl: window }); return true; } const count = parseInt(current); if (count >= limit) { return false; } await env.CACHE.put(key, String(count + 1), { expirationTtl: window }); return true; } export default { async fetch(request: Request, env: Env): Promise { const ip = request.headers.get("CF-Connecting-IP") || "unknown"; const allowed = await checkRateLimit(ip, env); if (!allowed) { return Response.json({ error: "Rate limit exceeded" }, { status: 429 }); } return handleRequest(request, env); }, }; ``` ### CSRF Protection Protect against cross-site request forgery. ```typescript function generateCSRFToken(): string { const array = new Uint8Array(32); crypto.getRandomValues(array); return btoa(String.fromCharCode(...array)); } async function validateCSRFToken( token: string, sessionId: string, env: Env ): Promise { const stored = await env.SESSIONS.get(`csrf:${sessionId}`); return stored === token; } export default { async fetch(request: Request, env: Env): Promise { if (request.method === "POST") { const sessionId = request.headers.get("X-Session-ID"); const csrfToken = request.headers.get("X-CSRF-Token"); if (!sessionId || !csrfToken) { return Response.json({ error: "Missing tokens" }, { status: 403 }); } const isValid = await validateCSRFToken(csrfToken, sessionId, env); if (!isValid) { return Response.json({ error: "Invalid CSRF token" }, { status: 403 }); } } return handleRequest(request, env); }, }; ``` ## Additional Resources - **Testing**: https://developers.cloudflare.com/workers/testing/ - **Observability**: https://developers.cloudflare.com/workers/observability/ - **Best Practices**: https://developers.cloudflare.com/workers/best-practices/ - **Examples**: https://developers.cloudflare.com/workers/examples/