mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 18:11:27 -07:00
update skills
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
# Cloudflare Pages Functions
|
||||
|
||||
Serverless functions on Cloudflare Pages using Workers runtime. Full-stack dev with file-based routing.
|
||||
|
||||
## Quick Navigation
|
||||
|
||||
**Need to...**
|
||||
| Task | Go to |
|
||||
|------|-------|
|
||||
| Set up TypeScript types | [configuration.md](./configuration.md) - TypeScript Setup |
|
||||
| Configure bindings (KV, D1, R2) | [configuration.md](./configuration.md) - wrangler.jsonc |
|
||||
| Access request/env/params | [api.md](./api.md) - EventContext |
|
||||
| Add middleware or auth | [patterns.md](./patterns.md) - Middleware, Auth |
|
||||
| Background tasks (waitUntil) | [patterns.md](./patterns.md) - Background Tasks |
|
||||
| Debug errors or check limits | [gotchas.md](./gotchas.md) - Common Errors, Limits |
|
||||
|
||||
## Decision Tree: Is This Pages Functions?
|
||||
|
||||
```
|
||||
Need serverless backend?
|
||||
├─ Yes, for a static site → Pages Functions
|
||||
├─ Yes, standalone API → Workers
|
||||
└─ Just static hosting → Pages (no functions)
|
||||
|
||||
Have existing Worker?
|
||||
├─ Complex routing logic → Use _worker.js (Advanced Mode)
|
||||
└─ Simple routes → Migrate to /functions (File-Based)
|
||||
|
||||
Framework-based?
|
||||
├─ Next.js/SvelteKit/Remix → Uses _worker.js automatically
|
||||
└─ Vanilla/HTML/React SPA → Use /functions
|
||||
```
|
||||
|
||||
## File-Based Routing
|
||||
|
||||
```
|
||||
/functions
|
||||
├── index.js → /
|
||||
├── api.js → /api
|
||||
├── users/
|
||||
│ ├── index.js → /users/
|
||||
│ ├── [user].js → /users/:user
|
||||
│ └── [[catchall]].js → /users/*
|
||||
└── _middleware.js → runs on all routes
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- `index.js` → directory root
|
||||
- Trailing slash optional
|
||||
- Specific routes precede catch-alls
|
||||
- Falls back to static if no match
|
||||
|
||||
## Dynamic Routes
|
||||
|
||||
**Single segment** `[param]` → string:
|
||||
```js
|
||||
// /functions/users/[user].js
|
||||
export function onRequest(context) {
|
||||
return new Response(`Hello ${context.params.user}`);
|
||||
}
|
||||
// Matches: /users/nevi
|
||||
```
|
||||
|
||||
**Multi-segment** `[[param]]` → array:
|
||||
```js
|
||||
// /functions/users/[[catchall]].js
|
||||
export function onRequest(context) {
|
||||
return new Response(JSON.stringify(context.params.catchall));
|
||||
}
|
||||
// Matches: /users/nevi/foobar → ["nevi", "foobar"]
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Method handlers:** `onRequestGet`, `onRequestPost`, etc.
|
||||
- **Middleware:** `_middleware.js` for cross-cutting concerns
|
||||
- **Bindings:** KV, D1, R2, Durable Objects, Workers AI, Service bindings
|
||||
- **TypeScript:** Full type support via `wrangler types` command
|
||||
- **Advanced mode:** Use `_worker.js` for custom routing logic
|
||||
|
||||
## Reading Order
|
||||
|
||||
**New to Pages Functions?** Start here:
|
||||
1. [README.md](./README.md) - Overview, routing, decision tree (you are here)
|
||||
2. [configuration.md](./configuration.md) - TypeScript setup, wrangler.jsonc, bindings
|
||||
3. [api.md](./api.md) - EventContext, handlers, bindings reference
|
||||
4. [patterns.md](./patterns.md) - Middleware, auth, CORS, rate limiting, caching
|
||||
5. [gotchas.md](./gotchas.md) - Common errors, debugging, limits
|
||||
|
||||
**Quick reference lookup:**
|
||||
- Bindings table → [api.md](./api.md)
|
||||
- Error diagnosis → [gotchas.md](./gotchas.md)
|
||||
- TypeScript setup → [configuration.md](./configuration.md)
|
||||
|
||||
## See Also
|
||||
- [pages](../pages/) - Pages platform overview and static site deployment
|
||||
- [workers](../workers/) - Workers runtime API reference
|
||||
- [d1](../d1/) - D1 database integration with Pages Functions
|
||||
@@ -0,0 +1,143 @@
|
||||
# Function API
|
||||
|
||||
## EventContext
|
||||
|
||||
```typescript
|
||||
interface EventContext<Env = any> {
|
||||
request: Request; // Incoming request
|
||||
functionPath: string; // Request path
|
||||
waitUntil(promise: Promise<any>): void; // Background tasks (non-blocking)
|
||||
passThroughOnException(): void; // Fallback to static on error
|
||||
next(input?: Request | string, init?: RequestInit): Promise<Response>;
|
||||
env: Env; // Bindings, vars, secrets
|
||||
params: Record<string, string | string[]>; // Route params ([user] or [[catchall]])
|
||||
data: any; // Middleware shared state
|
||||
}
|
||||
```
|
||||
|
||||
**TypeScript:** See [configuration.md](./configuration.md) for `wrangler types` setup
|
||||
|
||||
## Handlers
|
||||
|
||||
```typescript
|
||||
// Generic (fallback for any method)
|
||||
export async function onRequest(ctx: EventContext): Promise<Response> {
|
||||
return new Response('Any method');
|
||||
}
|
||||
|
||||
// Method-specific (takes precedence over generic)
|
||||
export async function onRequestGet(ctx: EventContext): Promise<Response> {
|
||||
return Response.json({ message: 'GET' });
|
||||
}
|
||||
|
||||
export async function onRequestPost(ctx: EventContext): Promise<Response> {
|
||||
const body = await ctx.request.json();
|
||||
return Response.json({ received: body });
|
||||
}
|
||||
// Also: onRequestPut, onRequestPatch, onRequestDelete, onRequestHead, onRequestOptions
|
||||
```
|
||||
|
||||
## Bindings Reference
|
||||
|
||||
| Binding Type | Interface | Config Key | Use Case |
|
||||
|--------------|-----------|------------|----------|
|
||||
| KV | `KVNamespace` | `kv_namespaces` | Key-value cache, sessions, config |
|
||||
| D1 | `D1Database` | `d1_databases` | Relational data, SQL queries |
|
||||
| R2 | `R2Bucket` | `r2_buckets` | Large files, user uploads, assets |
|
||||
| Durable Objects | `DurableObjectNamespace` | `durable_objects.bindings` | Stateful coordination, websockets |
|
||||
| Workers AI | `Ai` | `ai.binding` | LLM inference, embeddings |
|
||||
| Vectorize | `VectorizeIndex` | `vectorize` | Vector search, embeddings |
|
||||
| Service Binding | `Fetcher` | `services` | Worker-to-worker RPC |
|
||||
| Analytics Engine | `AnalyticsEngineDataset` | `analytics_engine_datasets` | Event logging, metrics |
|
||||
| Environment Vars | `string` | `vars` | Non-sensitive config |
|
||||
|
||||
See [configuration.md](./configuration.md) for wrangler.jsonc examples.
|
||||
|
||||
## Bindings
|
||||
|
||||
### KV
|
||||
|
||||
```typescript
|
||||
interface Env { KV: KVNamespace; }
|
||||
export const onRequest: PagesFunction<Env> = async (ctx) => {
|
||||
await ctx.env.KV.put('key', 'value', { expirationTtl: 3600 });
|
||||
const val = await ctx.env.KV.get('key', { type: 'json' });
|
||||
const keys = await ctx.env.KV.list({ prefix: 'user:' });
|
||||
return Response.json({ val });
|
||||
};
|
||||
```
|
||||
|
||||
### D1
|
||||
|
||||
```typescript
|
||||
interface Env { DB: D1Database; }
|
||||
export const onRequest: PagesFunction<Env> = async (ctx) => {
|
||||
const user = await ctx.env.DB.prepare('SELECT * FROM users WHERE id = ?').bind(123).first();
|
||||
return Response.json(user);
|
||||
};
|
||||
```
|
||||
|
||||
### R2
|
||||
|
||||
```typescript
|
||||
interface Env { BUCKET: R2Bucket; }
|
||||
export const onRequest: PagesFunction<Env> = async (ctx) => {
|
||||
const obj = await ctx.env.BUCKET.get('file.txt');
|
||||
if (!obj) return new Response('Not found', { status: 404 });
|
||||
await ctx.env.BUCKET.put('file.txt', ctx.request.body);
|
||||
return new Response(obj.body);
|
||||
};
|
||||
```
|
||||
|
||||
### Durable Objects
|
||||
|
||||
```typescript
|
||||
interface Env { COUNTER: DurableObjectNamespace; }
|
||||
export const onRequest: PagesFunction<Env> = async (ctx) => {
|
||||
const stub = ctx.env.COUNTER.get(ctx.env.COUNTER.idFromName('global'));
|
||||
return stub.fetch(ctx.request);
|
||||
};
|
||||
```
|
||||
|
||||
### Workers AI
|
||||
|
||||
```typescript
|
||||
interface Env { AI: Ai; }
|
||||
export const onRequest: PagesFunction<Env> = async (ctx) => {
|
||||
const resp = await ctx.env.AI.run('@cf/meta/llama-3.1-8b-instruct', { prompt: 'Hello' });
|
||||
return Response.json(resp);
|
||||
};
|
||||
```
|
||||
|
||||
### Service Bindings & Env Vars
|
||||
|
||||
```typescript
|
||||
interface Env { AUTH: Fetcher; API_KEY: string; }
|
||||
export const onRequest: PagesFunction<Env> = async (ctx) => {
|
||||
// Service binding: forward to another Worker
|
||||
return ctx.env.AUTH.fetch(ctx.request);
|
||||
|
||||
// Environment variable
|
||||
return Response.json({ key: ctx.env.API_KEY });
|
||||
};
|
||||
```
|
||||
|
||||
## Advanced Mode (env.ASSETS)
|
||||
|
||||
When using `_worker.js`, access static assets via `env.ASSETS.fetch()`:
|
||||
|
||||
```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); // Fallback to static
|
||||
}
|
||||
} satisfies ExportedHandler<Env>;
|
||||
```
|
||||
|
||||
**See also:** [configuration.md](./configuration.md) for TypeScript setup and wrangler.jsonc | [patterns.md](./patterns.md) for middleware and auth patterns
|
||||
@@ -0,0 +1,122 @@
|
||||
# Configuration
|
||||
|
||||
## TypeScript Setup
|
||||
|
||||
**Generate types from wrangler.jsonc** (replaces deprecated `@cloudflare/workers-types`):
|
||||
|
||||
```bash
|
||||
npx wrangler types
|
||||
```
|
||||
|
||||
Creates `worker-configuration.d.ts` with typed `Env` interface based on your bindings.
|
||||
|
||||
```typescript
|
||||
// functions/api.ts
|
||||
export const onRequest: PagesFunction<Env> = async (ctx) => {
|
||||
// ctx.env.KV, ctx.env.DB, etc. are fully typed
|
||||
return Response.json({ ok: true });
|
||||
};
|
||||
```
|
||||
|
||||
**Manual types** (if not using wrangler types):
|
||||
|
||||
```typescript
|
||||
interface Env {
|
||||
KV: KVNamespace;
|
||||
DB: D1Database;
|
||||
API_KEY: string;
|
||||
}
|
||||
export const onRequest: PagesFunction<Env> = async (ctx) => { /* ... */ };
|
||||
```
|
||||
|
||||
## wrangler.jsonc
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"name": "my-pages-app",
|
||||
"pages_build_output_dir": "./dist",
|
||||
"compatibility_date": "2025-01-01",
|
||||
"compatibility_flags": ["nodejs_compat"],
|
||||
|
||||
"vars": { "API_URL": "https://api.example.com" },
|
||||
"kv_namespaces": [{ "binding": "KV", "id": "abc123" }],
|
||||
"d1_databases": [{ "binding": "DB", "database_name": "prod-db", "database_id": "xyz789" }],
|
||||
"r2_buckets": [{ "binding": "BUCKET", "bucket_name": "my-bucket" }],
|
||||
"durable_objects": { "bindings": [{ "name": "COUNTER", "class_name": "Counter", "script_name": "counter-worker" }] },
|
||||
"services": [{ "binding": "AUTH", "service": "auth-worker" }],
|
||||
"ai": { "binding": "AI" },
|
||||
"vectorize": [{ "binding": "VECTORIZE", "index_name": "my-index" }],
|
||||
"analytics_engine_datasets": [{ "binding": "ANALYTICS" }]
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Overrides
|
||||
|
||||
Top-level → local dev, `env.preview` → preview, `env.production` → production
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"vars": { "API_URL": "http://localhost:8787" },
|
||||
"env": {
|
||||
"production": { "vars": { "API_URL": "https://api.example.com" } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** If overriding `vars`, `kv_namespaces`, `d1_databases`, etc., ALL must be redefined (non-inheritable)
|
||||
|
||||
## Local Secrets (.dev.vars)
|
||||
|
||||
**Local dev only** - NOT deployed:
|
||||
|
||||
```bash
|
||||
# .dev.vars (add to .gitignore)
|
||||
SECRET_KEY="my-secret-value"
|
||||
```
|
||||
|
||||
Accessed via `ctx.env.SECRET_KEY`. Set production secrets:
|
||||
```bash
|
||||
echo "value" | npx wrangler pages secret put SECRET_KEY --project-name=my-app
|
||||
```
|
||||
|
||||
## Static Config Files
|
||||
|
||||
**_routes.json** - Custom routing:
|
||||
```json
|
||||
{ "version": 1, "include": ["/api/*"], "exclude": ["/static/*"] }
|
||||
```
|
||||
|
||||
**_headers** - Static headers:
|
||||
```
|
||||
/static/*
|
||||
Cache-Control: public, max-age=31536000
|
||||
```
|
||||
|
||||
**_redirects** - Redirects:
|
||||
```
|
||||
/old /new 301
|
||||
```
|
||||
|
||||
## Local Dev & Deployment
|
||||
|
||||
```bash
|
||||
# Dev server
|
||||
npx wrangler pages dev ./dist
|
||||
|
||||
# With bindings
|
||||
npx wrangler pages dev ./dist --kv=KV --d1=DB=db-id --r2=BUCKET
|
||||
|
||||
# Durable Objects (2 terminals)
|
||||
cd do-worker && npx wrangler dev
|
||||
cd pages-project && npx wrangler pages dev ./dist --do COUNTER=Counter@do-worker
|
||||
|
||||
# Deploy
|
||||
npx wrangler pages deploy ./dist
|
||||
npx wrangler pages deploy ./dist --branch preview
|
||||
|
||||
# Download config
|
||||
npx wrangler pages download config my-project
|
||||
```
|
||||
|
||||
**See also:** [api.md](./api.md) for binding usage examples
|
||||
@@ -0,0 +1,94 @@
|
||||
# Gotchas & Debugging
|
||||
|
||||
## Error Diagnosis
|
||||
|
||||
| Symptom | Likely Cause | Solution |
|
||||
|---------|--------------|----------|
|
||||
| **Function not invoking** | Wrong `/functions` location, wrong extension, or `_routes.json` excludes path | Check `pages_build_output_dir`, use `.js`/`.ts`, verify `_routes.json` |
|
||||
| **`ctx.env.BINDING` undefined** | Binding not configured or name mismatch | Add to `wrangler.jsonc`, verify exact name (case-sensitive), redeploy |
|
||||
| **TypeScript errors on `ctx.env`** | Missing type definition | Run `wrangler types` or define `interface Env {}` |
|
||||
| **Middleware not running** | Wrong filename/location or missing `ctx.next()` | Name exactly `_middleware.js`, export `onRequest`, call `ctx.next()` |
|
||||
| **Secrets missing in production** | `.dev.vars` not deployed | `.dev.vars` is local only - set production secrets via dashboard or `wrangler secret put` |
|
||||
| **Type mismatch on binding** | Wrong interface type | See [api.md](./api.md) bindings table for correct types |
|
||||
| **"KV key not found" but exists** | Key in wrong namespace or env | Verify namespace binding, check preview vs production env |
|
||||
| **Function times out** | Synchronous wait or missing `await` | All I/O must be async/await, use `ctx.waitUntil()` for background tasks |
|
||||
|
||||
## Common Errors
|
||||
|
||||
### TypeScript type errors
|
||||
|
||||
**Problem:** `ctx.env.MY_BINDING` shows type error
|
||||
**Cause:** No type definition for `Env`
|
||||
**Solution:** Run `npx wrangler types` or manually define:
|
||||
```typescript
|
||||
interface Env { MY_BINDING: KVNamespace; }
|
||||
export const onRequest: PagesFunction<Env> = async (ctx) => { /* ... */ };
|
||||
```
|
||||
|
||||
### Secrets not available in production
|
||||
|
||||
**Problem:** `ctx.env.SECRET_KEY` is undefined in production
|
||||
**Cause:** `.dev.vars` is local-only, not deployed
|
||||
**Solution:** Set production secrets:
|
||||
```bash
|
||||
echo "value" | npx wrangler pages secret put SECRET_KEY --project-name=my-app
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
```typescript
|
||||
// Console logging
|
||||
export async function onRequest(ctx) {
|
||||
console.log('Request:', ctx.request.method, ctx.request.url);
|
||||
const res = await ctx.next();
|
||||
console.log('Status:', res.status);
|
||||
return res;
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
# Stream real-time logs
|
||||
npx wrangler pages deployment tail
|
||||
npx wrangler pages deployment tail --status error
|
||||
```
|
||||
|
||||
```jsonc
|
||||
// Source maps (wrangler.jsonc)
|
||||
{ "upload_source_maps": true }
|
||||
```
|
||||
|
||||
## Limits
|
||||
|
||||
| Resource | Free | Paid |
|
||||
|----------|------|------|
|
||||
| CPU time | 10ms | 50ms |
|
||||
| Memory | 128 MB | 128 MB |
|
||||
| Script size | 10 MB compressed | 10 MB compressed |
|
||||
| Env vars | 5 KB per var, 64 max | 5 KB per var, 64 max |
|
||||
| Requests | 100k/day | Unlimited ($0.50/million) |
|
||||
|
||||
## Best Practices
|
||||
|
||||
**Performance:** Minimize deps (cold start), use KV for cache/D1 for relational/R2 for large files, set `Cache-Control` headers, batch DB ops, handle errors gracefully
|
||||
|
||||
**Security:** Never commit secrets (use `.dev.vars` + gitignore), validate input, sanitize before DB, implement auth middleware, set CORS headers, rate limit per-IP
|
||||
|
||||
## Migration
|
||||
|
||||
**Workers → Pages Functions:**
|
||||
- `export default { fetch(req, env) {} }` → `export function onRequest(ctx) { const { request, env } = ctx; }`
|
||||
- Use `_worker.js` for complex routing: `env.ASSETS.fetch(request)` for static files
|
||||
|
||||
**Other platforms → Pages:**
|
||||
- File-based routing: `/functions/api/users.js` → `/api/users`
|
||||
- Dynamic routes: `[param]` not `:param`
|
||||
- Replace Node.js deps with Workers APIs or add `nodejs_compat` flag
|
||||
|
||||
## Resources
|
||||
|
||||
- [Official Docs](https://developers.cloudflare.com/pages/functions/)
|
||||
- [Workers APIs](https://developers.cloudflare.com/workers/runtime-apis/)
|
||||
- [Examples](https://github.com/cloudflare/pages-example-projects)
|
||||
- [Discord](https://discord.gg/cloudflaredev)
|
||||
|
||||
**See also:** [configuration.md](./configuration.md) for TypeScript setup | [patterns.md](./patterns.md) for middleware/auth | [api.md](./api.md) for bindings
|
||||
@@ -0,0 +1,137 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user