mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 06:11:27 -07:00
138 lines
4.1 KiB
Markdown
138 lines
4.1 KiB
Markdown
# Common Patterns
|
|
|
|
## Background Tasks (waitUntil)
|
|
|
|
Non-blocking tasks after response sent (analytics, cleanup, webhooks):
|
|
|
|
```typescript
|
|
export async function onRequest(ctx: EventContext<Env>) {
|
|
const res = Response.json({ success: true });
|
|
|
|
ctx.waitUntil(ctx.env.KV.put('last-visit', new Date().toISOString()));
|
|
ctx.waitUntil(Promise.all([
|
|
ctx.env.ANALYTICS.writeDataPoint({ event: 'view' }),
|
|
fetch('https://webhook.site/...', { method: 'POST' })
|
|
]));
|
|
|
|
return res; // Returned immediately
|
|
}
|
|
```
|
|
|
|
## Middleware & Auth
|
|
|
|
```typescript
|
|
// functions/_middleware.js (global) or functions/users/_middleware.js (scoped)
|
|
export async function onRequest(ctx) {
|
|
try { return await ctx.next(); }
|
|
catch (err) { return new Response(err.message, { status: 500 }); }
|
|
}
|
|
|
|
// Chained: export const onRequest = [errorHandler, auth, logger];
|
|
|
|
// Auth
|
|
async function auth(ctx: EventContext<Env>) {
|
|
const token = ctx.request.headers.get('authorization')?.replace('Bearer ', '');
|
|
if (!token) return new Response('Unauthorized', { status: 401 });
|
|
const session = await ctx.env.KV.get(`session:${token}`);
|
|
if (!session) return new Response('Invalid', { status: 401 });
|
|
ctx.data.user = JSON.parse(session);
|
|
return ctx.next();
|
|
}
|
|
```
|
|
|
|
## CORS & Rate Limiting
|
|
|
|
```typescript
|
|
// CORS middleware
|
|
const cors = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST' };
|
|
export async function onRequestOptions() { return new Response(null, { headers: cors }); }
|
|
export async function onRequest(ctx) {
|
|
const res = await ctx.next();
|
|
Object.entries(cors).forEach(([k, v]) => res.headers.set(k, v));
|
|
return res;
|
|
}
|
|
|
|
// Rate limiting (KV-based)
|
|
async function rateLimit(ctx: EventContext<Env>) {
|
|
const ip = ctx.request.headers.get('CF-Connecting-IP') || 'unknown';
|
|
const count = parseInt(await ctx.env.KV.get(`rate:${ip}`) || '0');
|
|
if (count >= 100) return new Response('Rate limited', { status: 429 });
|
|
await ctx.env.KV.put(`rate:${ip}`, (count + 1).toString(), { expirationTtl: 3600 });
|
|
return ctx.next();
|
|
}
|
|
```
|
|
|
|
## Forms, Caching, Redirects
|
|
|
|
```typescript
|
|
// JSON & file upload
|
|
export async function onRequestPost(ctx) {
|
|
const ct = ctx.request.headers.get('content-type') || '';
|
|
if (ct.includes('application/json')) return Response.json(await ctx.request.json());
|
|
if (ct.includes('multipart/form-data')) {
|
|
const file = (await ctx.request.formData()).get('file') as File;
|
|
await ctx.env.BUCKET.put(file.name, file.stream());
|
|
return Response.json({ uploaded: file.name });
|
|
}
|
|
}
|
|
|
|
// Cache API
|
|
export async function onRequest(ctx) {
|
|
let res = await caches.default.match(ctx.request);
|
|
if (!res) {
|
|
res = new Response('Data');
|
|
res.headers.set('Cache-Control', 'public, max-age=3600');
|
|
ctx.waitUntil(caches.default.put(ctx.request, res.clone()));
|
|
}
|
|
return res;
|
|
}
|
|
|
|
// Redirects
|
|
export async function onRequest(ctx) {
|
|
if (new URL(ctx.request.url).pathname === '/old') {
|
|
return Response.redirect(new URL('/new', ctx.request.url), 301);
|
|
}
|
|
return ctx.next();
|
|
}
|
|
```
|
|
|
|
## Testing
|
|
|
|
**Unit tests** (Vitest + cloudflare:test):
|
|
```typescript
|
|
import { env } from 'cloudflare:test';
|
|
import { it, expect } from 'vitest';
|
|
import { onRequest } from '../functions/api';
|
|
|
|
it('returns JSON', async () => {
|
|
const req = new Request('http://localhost/api');
|
|
const ctx = { request: req, env, params: {}, data: {} } as EventContext;
|
|
const res = await onRequest(ctx);
|
|
expect(res.status).toBe(200);
|
|
});
|
|
```
|
|
|
|
**Integration:** `wrangler pages dev` + Playwright/Cypress
|
|
|
|
## Advanced Mode (_worker.js)
|
|
|
|
Use `_worker.js` for complex routing (replaces `/functions`):
|
|
|
|
```typescript
|
|
interface Env { ASSETS: Fetcher; KV: KVNamespace; }
|
|
|
|
export default {
|
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
const url = new URL(request.url);
|
|
if (url.pathname.startsWith('/api/')) {
|
|
return Response.json({ data: await env.KV.get('key') });
|
|
}
|
|
return env.ASSETS.fetch(request); // Static files
|
|
}
|
|
} satisfies ExportedHandler<Env>;
|
|
```
|
|
|
|
**When:** Existing Worker, framework-generated (Next.js/SvelteKit), custom routing logic
|
|
|
|
**See also:** [api.md](./api.md) for `env.ASSETS.fetch()` | [gotchas.md](./gotchas.md) for debugging
|