Initial commit
This commit is contained in:
805
skills/skill/references/development-patterns.md
Normal file
805
skills/skill/references/development-patterns.md
Normal file
@@ -0,0 +1,805 @@
|
||||
# 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<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.
|
||||
|
||||
```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<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.
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```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<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.
|
||||
|
||||
```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<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.
|
||||
|
||||
```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<Response> {
|
||||
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<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.
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
```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<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.
|
||||
|
||||
```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<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.
|
||||
|
||||
```typescript
|
||||
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.
|
||||
|
||||
```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<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
|
||||
|
||||
- **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/
|
||||
Reference in New Issue
Block a user