Files
gh-tenequm-claude-plugins-c…/skills/skill/references/development-patterns.md
2025-11-30 09:01:09 +08:00

17 KiB

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:

npm install -D vitest @cloudflare/vitest-pool-workers

vitest.config.ts:

import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config";

export default defineWorkersConfig({
  test: {
    poolOptions: {
      workers: {
        wrangler: { configPath: "./wrangler.toml" },
      },
    },
  },
});

Basic Test:

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:

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:

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:

npm test
# or
npx vitest

Integration Testing

Test full request/response cycles with external services.

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.

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.

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    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<Response> {
  // Your handler logic
}

Custom Error Classes

Define custom error types for better error handling.

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<string, string>) {
    super("Validation failed");
    this.name = "ValidationError";
  }
}

async function handleRequest(request: Request, env: Env): Promise<Response> {
  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.

async function fetchWithRetry(
  url: string,
  options: RequestInit = {},
  maxRetries = 3
): Promise<Response> {
  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.

async function handleCachedRequest(request: Request): Promise<Response> {
  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:

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.

export default {
  async fetch(request: Request): Promise<Response> {
    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.

// 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.

export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    // 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.

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.

# 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:

{
  "compilerOptions": {
    "sourceMap": true
  }
}

wrangler.toml:

upload_source_maps = true

Local Debugging

Use DevTools for debugging.

# Start with inspector
wrangler dev --inspector

Then open chrome://inspect in Chrome and connect to the worker.

Breakpoints

Set breakpoints in your code.

export default {
  async fetch(request: Request): Promise<Response> {
    debugger;  // Execution will pause here

    const data = await fetchData();
    return Response.json(data);
  },
};

Code Organization

Router Pattern

Organize routes cleanly.

interface Route {
  pattern: URLPattern;
  handler: (request: Request, env: Env, params: URLPatternResult) => Promise<Response>;
}

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<Response> {
    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<Response> {
  const users = await env.DB.prepare("SELECT * FROM users").all();
  return Response.json(users.results);
}

Middleware Pattern

Chain middleware for cross-cutting concerns.

type Middleware = (
  request: Request,
  env: Env,
  next: () => Promise<Response>
) => Promise<Response>;

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<Response>,
  middlewares: Middleware[]
): (request: Request, env: Env) => Promise<Response> {
  return (request: Request, env: Env) => {
    let index = -1;

    const dispatch = async (i: number): Promise<Response> => {
      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.

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<Response> {
    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.

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<Response> {
    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.

async function checkRateLimit(
  identifier: string,
  env: Env,
  limit = 100,
  window = 60
): Promise<boolean> {
  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<Response> {
    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.

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<boolean> {
  const stored = await env.SESSIONS.get(`csrf:${sessionId}`);
  return stored === token;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    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