419 lines
8.6 KiB
Markdown
419 lines
8.6 KiB
Markdown
# Cloudflare Workers Basics
|
|
|
|
Getting started with Cloudflare Workers: serverless functions that run on edge network across 300+ cities.
|
|
|
|
## Handler Types
|
|
|
|
### Fetch Handler (HTTP Requests)
|
|
```typescript
|
|
export default {
|
|
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
|
return new Response('Hello World!');
|
|
}
|
|
};
|
|
```
|
|
|
|
### Scheduled Handler (Cron Jobs)
|
|
```typescript
|
|
export default {
|
|
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
|
|
await fetch('https://api.example.com/cleanup');
|
|
}
|
|
};
|
|
```
|
|
|
|
**Configure in wrangler.toml:**
|
|
```toml
|
|
[triggers]
|
|
crons = ["0 0 * * *"] # Daily at midnight
|
|
```
|
|
|
|
### Queue Handler (Message Processing)
|
|
```typescript
|
|
export default {
|
|
async queue(batch: MessageBatch, env: Env, ctx: ExecutionContext): Promise<void> {
|
|
for (const message of batch.messages) {
|
|
await processMessage(message.body);
|
|
message.ack(); // Acknowledge success
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
### Email Handler (Email Routing)
|
|
```typescript
|
|
export default {
|
|
async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext): Promise<void> {
|
|
await message.forward('destination@example.com');
|
|
}
|
|
};
|
|
```
|
|
|
|
## Request/Response Basics
|
|
|
|
### Parsing Request
|
|
```typescript
|
|
const url = new URL(request.url);
|
|
const method = request.method;
|
|
const headers = request.headers;
|
|
|
|
// Query parameters
|
|
const name = url.searchParams.get('name');
|
|
|
|
// JSON body
|
|
const data = await request.json();
|
|
|
|
// Text body
|
|
const text = await request.text();
|
|
|
|
// Form data
|
|
const formData = await request.formData();
|
|
```
|
|
|
|
### Creating Response
|
|
```typescript
|
|
// Text response
|
|
return new Response('Hello', { status: 200 });
|
|
|
|
// JSON response
|
|
return new Response(JSON.stringify({ message: 'Hello' }), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' }
|
|
});
|
|
|
|
// Stream response
|
|
return new Response(readable, {
|
|
headers: { 'Content-Type': 'text/plain' }
|
|
});
|
|
|
|
// Redirect
|
|
return Response.redirect('https://example.com', 302);
|
|
```
|
|
|
|
## Routing Patterns
|
|
|
|
### URL-Based Routing
|
|
```typescript
|
|
export default {
|
|
async fetch(request: Request): Promise<Response> {
|
|
const url = new URL(request.url);
|
|
|
|
switch (url.pathname) {
|
|
case '/':
|
|
return new Response('Home');
|
|
case '/about':
|
|
return new Response('About');
|
|
default:
|
|
return new Response('Not Found', { status: 404 });
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
### Using Hono Framework (Recommended)
|
|
```typescript
|
|
import { Hono } from 'hono';
|
|
|
|
const app = new Hono();
|
|
|
|
app.get('/', (c) => c.text('Home'));
|
|
app.get('/api/users/:id', async (c) => {
|
|
const id = c.req.param('id');
|
|
const user = await getUser(id);
|
|
return c.json(user);
|
|
});
|
|
|
|
export default app;
|
|
```
|
|
|
|
## Working with Bindings
|
|
|
|
### Environment Variables
|
|
```toml
|
|
# wrangler.toml
|
|
[vars]
|
|
API_URL = "https://api.example.com"
|
|
```
|
|
|
|
```typescript
|
|
const apiUrl = env.API_URL;
|
|
```
|
|
|
|
### KV Namespace
|
|
```typescript
|
|
// Put with TTL
|
|
await env.KV.put('session:token', JSON.stringify(data), {
|
|
expirationTtl: 3600
|
|
});
|
|
|
|
// Get
|
|
const data = await env.KV.get('session:token', 'json');
|
|
|
|
// Delete
|
|
await env.KV.delete('session:token');
|
|
|
|
// List with prefix
|
|
const list = await env.KV.list({ prefix: 'user:123:' });
|
|
```
|
|
|
|
### D1 Database
|
|
```typescript
|
|
// Query
|
|
const result = await env.DB.prepare(
|
|
'SELECT * FROM users WHERE id = ?'
|
|
).bind(userId).first();
|
|
|
|
// Insert
|
|
await env.DB.prepare(
|
|
'INSERT INTO users (name, email) VALUES (?, ?)'
|
|
).bind('Alice', 'alice@example.com').run();
|
|
|
|
// Batch (atomic)
|
|
await env.DB.batch([
|
|
env.DB.prepare('UPDATE accounts SET balance = balance - 100 WHERE id = ?').bind(1),
|
|
env.DB.prepare('UPDATE accounts SET balance = balance + 100 WHERE id = ?').bind(2)
|
|
]);
|
|
```
|
|
|
|
### R2 Bucket
|
|
```typescript
|
|
// Put object
|
|
await env.R2_BUCKET.put('path/to/file.jpg', fileBuffer, {
|
|
httpMetadata: {
|
|
contentType: 'image/jpeg'
|
|
}
|
|
});
|
|
|
|
// Get object
|
|
const object = await env.R2_BUCKET.get('path/to/file.jpg');
|
|
if (!object) {
|
|
return new Response('Not found', { status: 404 });
|
|
}
|
|
|
|
// Stream response
|
|
return new Response(object.body, {
|
|
headers: {
|
|
'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream'
|
|
}
|
|
});
|
|
|
|
// Delete
|
|
await env.R2_BUCKET.delete('path/to/file.jpg');
|
|
```
|
|
|
|
## Context API
|
|
|
|
### waitUntil (Background Tasks)
|
|
```typescript
|
|
export default {
|
|
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
|
// Run analytics after response sent
|
|
ctx.waitUntil(
|
|
fetch('https://analytics.example.com/log', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ url: request.url })
|
|
})
|
|
);
|
|
|
|
return new Response('OK');
|
|
}
|
|
};
|
|
```
|
|
|
|
### passThroughOnException
|
|
```typescript
|
|
// Continue to origin on error
|
|
ctx.passThroughOnException();
|
|
|
|
// Your code that might throw
|
|
const data = await riskyOperation();
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
```typescript
|
|
export default {
|
|
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
|
try {
|
|
const response = await processRequest(request, env);
|
|
return response;
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
|
|
// Log to external service
|
|
ctx.waitUntil(
|
|
fetch('https://logging.example.com/error', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
error: error.message,
|
|
url: request.url
|
|
})
|
|
})
|
|
);
|
|
|
|
return new Response('Internal Server Error', { status: 500 });
|
|
}
|
|
}
|
|
};
|
|
```
|
|
|
|
## CORS
|
|
|
|
```typescript
|
|
function corsHeaders(origin: string) {
|
|
return {
|
|
'Access-Control-Allow-Origin': origin,
|
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
'Access-Control-Max-Age': '86400'
|
|
};
|
|
}
|
|
|
|
export default {
|
|
async fetch(request: Request): Promise<Response> {
|
|
const origin = request.headers.get('Origin') || '*';
|
|
|
|
// Handle preflight
|
|
if (request.method === 'OPTIONS') {
|
|
return new Response(null, { headers: corsHeaders(origin) });
|
|
}
|
|
|
|
// Handle request
|
|
const response = await handleRequest(request);
|
|
const headers = new Headers(response.headers);
|
|
Object.entries(corsHeaders(origin)).forEach(([key, value]) => {
|
|
headers.set(key, value);
|
|
});
|
|
|
|
return new Response(response.body, {
|
|
status: response.status,
|
|
headers
|
|
});
|
|
}
|
|
};
|
|
```
|
|
|
|
## Cache API
|
|
|
|
```typescript
|
|
export default {
|
|
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
|
const cache = caches.default;
|
|
const cacheKey = new Request(request.url);
|
|
|
|
// Check cache
|
|
let response = await cache.match(cacheKey);
|
|
if (response) return response;
|
|
|
|
// Fetch from origin
|
|
response = await fetch(request);
|
|
|
|
// Cache response
|
|
ctx.waitUntil(cache.put(cacheKey, response.clone()));
|
|
|
|
return response;
|
|
}
|
|
};
|
|
```
|
|
|
|
## Secrets Management
|
|
|
|
```bash
|
|
# Add secret
|
|
wrangler secret put API_KEY
|
|
# Enter value when prompted
|
|
|
|
# Use in Worker
|
|
const apiKey = env.API_KEY;
|
|
```
|
|
|
|
## Local Development
|
|
|
|
```bash
|
|
# Start local dev server
|
|
wrangler dev
|
|
|
|
# Test with remote edge
|
|
wrangler dev --remote
|
|
|
|
# Custom port
|
|
wrangler dev --port 8080
|
|
|
|
# Access at http://localhost:8787
|
|
```
|
|
|
|
## Deployment
|
|
|
|
```bash
|
|
# Deploy to production
|
|
wrangler deploy
|
|
|
|
# Deploy to specific environment
|
|
wrangler deploy --env staging
|
|
|
|
# Preview deployment
|
|
wrangler deploy --dry-run
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### API Gateway
|
|
```typescript
|
|
import { Hono } from 'hono';
|
|
|
|
const app = new Hono();
|
|
|
|
app.get('/api/users', async (c) => {
|
|
const users = await c.env.DB.prepare('SELECT * FROM users').all();
|
|
return c.json(users.results);
|
|
});
|
|
|
|
app.post('/api/users', async (c) => {
|
|
const { name, email } = await c.req.json();
|
|
await c.env.DB.prepare(
|
|
'INSERT INTO users (name, email) VALUES (?, ?)'
|
|
).bind(name, email).run();
|
|
return c.json({ success: true }, 201);
|
|
});
|
|
|
|
export default app;
|
|
```
|
|
|
|
### Rate Limiting
|
|
```typescript
|
|
async function rateLimit(ip: string, env: Env): Promise<boolean> {
|
|
const key = `ratelimit:${ip}`;
|
|
const limit = 100;
|
|
const window = 60;
|
|
|
|
const current = await env.KV.get(key);
|
|
const count = current ? parseInt(current) : 0;
|
|
|
|
if (count >= limit) return false;
|
|
|
|
await env.KV.put(key, (count + 1).toString(), {
|
|
expirationTtl: window
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
export default {
|
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
const ip = request.headers.get('CF-Connecting-IP') || 'unknown';
|
|
|
|
if (!await rateLimit(ip, env)) {
|
|
return new Response('Rate limit exceeded', { status: 429 });
|
|
}
|
|
|
|
return new Response('OK');
|
|
}
|
|
};
|
|
```
|
|
|
|
## Resources
|
|
|
|
- Docs: https://developers.cloudflare.com/workers/
|
|
- Examples: https://developers.cloudflare.com/workers/examples/
|
|
- Runtime APIs: https://developers.cloudflare.com/workers/runtime-apis/
|