Curated Skills
by lstudlo

cloudflare

references/pages/patterns.md

.md 205 lines
Content
# Patterns

## API Routes

```typescript
// functions/api/todos/[id].ts
export const onRequestGet: PagesFunction<Env> = async ({ env, params }) => {
  const todo = await env.DB.prepare('SELECT * FROM todos WHERE id = ?').bind(params.id).first();
  if (!todo) return new Response('Not found', { status: 404 });
  return Response.json(todo);
};

export const onRequestPut: PagesFunction<Env> = async ({ env, params, request }) => {
  const body = await request.json();
  await env.DB.prepare('UPDATE todos SET title = ?, completed = ? WHERE id = ?')
    .bind(body.title, body.completed, params.id).run();
  return Response.json({ success: true });
};
// Also: onRequestDelete, onRequestPost
```

## Auth Middleware

```typescript
// functions/_middleware.ts
const auth: PagesFunction<Env> = async (context) => {
  if (context.request.url.includes('/public/')) return context.next();
  const authHeader = context.request.headers.get('Authorization');
  if (!authHeader?.startsWith('Bearer ')) {
    return new Response('Unauthorized', { status: 401 });
  }
  
  try {
    const payload = await verifyJWT(authHeader.substring(7), context.env.JWT_SECRET);
    context.data.user = payload;
    return context.next();
  } catch (err) {
    return new Response('Invalid token', { status: 401 });
  }
};
export const onRequest = [auth];
```

## CORS

```typescript
// functions/api/_middleware.ts
const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization'
};

export const onRequest: PagesFunction = async (context) => {
  if (context.request.method === 'OPTIONS') {
    return new Response(null, {headers: corsHeaders});
  }
  const response = await context.next();
  Object.entries(corsHeaders).forEach(([k, v]) => response.headers.set(k, v));
  return response;
};
```

## Form Handling

```typescript
// functions/api/contact.ts
export const onRequestPost: PagesFunction<Env> = async ({ request, env }) => {
  const formData = await request.formData();
  await env.QUEUE.send({name: formData.get('name'), email: formData.get('email')});
  return new Response('<h1>Thanks!</h1>', { headers: { 'Content-Type': 'text/html' } });
};
```

## Background Tasks

```typescript
export const onRequestPost: PagesFunction = async ({ request, waitUntil }) => {
  const data = await request.json();
  waitUntil(fetch('https://api.example.com/webhook', {
    method: 'POST', body: JSON.stringify(data)
  }));
  return Response.json({ queued: true });
};
```

## Error Handling

```typescript
// functions/_middleware.ts
const errorHandler: PagesFunction = async (context) => {
  try {
    return await context.next();
  } catch (error) {
    console.error('Error:', error);
    if (context.request.url.includes('/api/')) {
      return Response.json({ error: error.message }, { status: 500 });
    }
    return new Response(`<h1>Error</h1><p>${error.message}</p>`, { 
      status: 500, headers: { 'Content-Type': 'text/html' } 
    });
  }
};
export const onRequest = [errorHandler];
```

## Caching

```typescript
// functions/api/data.ts
export const onRequestGet: PagesFunction<Env> = async ({ env, request }) => {
  const cacheKey = `data:${new URL(request.url).pathname}`;
  const cached = await env.KV.get(cacheKey, 'json');
  if (cached) return Response.json(cached, { headers: { 'X-Cache': 'HIT' } });
  
  const data = await env.DB.prepare('SELECT * FROM data').first();
  await env.KV.put(cacheKey, JSON.stringify(data), {expirationTtl: 3600});
  return Response.json(data, {headers: {'X-Cache': 'MISS'}});
};
```

## Smart Placement for Database Apps

Enable Smart Placement for apps with D1 or centralized data sources:

```jsonc
// wrangler.jsonc
{
  "name": "global-app",
  "placement": {
    "mode": "smart"
  },
  "d1_databases": [{
    "binding": "DB",
    "database_id": "your-db-id"
  }]
}
```

```typescript
// functions/api/data.ts
export const onRequestGet: PagesFunction<Env> = async ({ env }) => {
  // Smart Placement optimizes execution location over time
  // Balances user location vs database location
  const data = await env.DB.prepare('SELECT * FROM products LIMIT 10').all();
  return Response.json(data);
};
```

**Best for**: Read-heavy apps with D1/Durable Objects in specific regions.  
**Not needed**: Apps without data locality constraints or with evenly distributed traffic.

## Framework Integration

**Supported** (2026): SvelteKit, Astro, Nuxt, Qwik, Solid Start

```bash
npm create cloudflare@latest my-app -- --framework=svelte
```

### SvelteKit
```typescript
// src/routes/+page.server.ts
export const load = async ({ platform }) => {
  const todos = await platform.env.DB.prepare('SELECT * FROM todos').all();
  return { todos: todos.results };
};
```

### Astro
```astro
---
const { DB } = Astro.locals.runtime.env;
const todos = await DB.prepare('SELECT * FROM todos').all();
---
<ul>{todos.results.map(t => <li>{t.title}</li>)}</ul>
```

### Nuxt
```typescript
// server/api/todos.get.ts
export default defineEventHandler(async (event) => {
  const { DB } = event.context.cloudflare.env;
  return await DB.prepare('SELECT * FROM todos').all();
});
```

**⚠️ Framework Status** (2026):
- ✅ **Supported**: SvelteKit, Astro, Nuxt, Qwik, Solid Start
- ❌ **Deprecated**: Next.js (`@cloudflare/next-on-pages`), Remix (`@remix-run/cloudflare-pages`)

For deprecated frameworks, see [gotchas.md](./gotchas.md#framework-specific) for migration options.

[Framework Guides](https://developers.cloudflare.com/pages/framework-guides/)

## Monorepo

Dashboard → Settings → Build → Root directory. Set to subproject (e.g., `apps/web`).

## Best Practices

**Performance**: Exclude static via `_routes.json`; cache with KV; keep bundle < 1MB  
**Security**: Use secrets (not vars); validate inputs; rate limit with KV/DO  
**Workflow**: Preview per branch; local dev with `wrangler pages dev`; instant rollbacks in Dashboard