Instruction file imported from djbriane/mailgun-edge-examples (
.cursor/rules/20-cloudflare.mdc). Copyright stays with the author.
Platform: Cloudflare Workers
Runtime: V8 isolates with Web-standard APIs
File Structure
Follow this pattern for all examples:
cloudflare/
├── example-name/
│ ├── src/
│ │ └── index.ts # Main worker code
│ ├── wrangler.toml # Configuration
│ └── README.md # Documentation
Worker Module Format
Use ES Modules syntax (modern standard):
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// Handle CORS preflight
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'content-type, authorization',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
},
})
}
try {
// Worker logic here
return new Response(
JSON.stringify({ success: true }),
{
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
},
)
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{
status: 400,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
},
)
}
},
}
TypeScript Types
Define your Env interface for environment variables:
interface Env {
API_KEY: string
MAILGUN_DOMAIN: string
// Add all your secrets/bindings here
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const apiKey = env.API_KEY
// ...
},
}
Worker Template
Use this as starting point for new workers:
interface Env {
API_KEY: string
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
// CORS preflight
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'content-type, authorization',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
},
})
}
try {
// 1. Parse request
const { param } = await request.json() as { param: string }
// 2. Get API credentials from env
if (!env.API_KEY) {
throw new Error('API_KEY not configured')
}
// 3. Call external API using fetch
const response = await fetch('https://api.example.com/endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${env.API_KEY}`,
},
body: JSON.stringify({ param }),
})
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`)
}
const data = await response.json()
// 4. Return result with CORS headers
return new Response(
JSON.stringify(data),
{
status: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
},
)
} catch (error) {
return new Response(
JSON.stringify({ error: error.message }),
{
status: 400,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
},
)
}
},
}
Configuration: wrangler.toml
Create a minimal wrangler.toml for each worker:
name = "example-name"
main = "src/index.ts"
compatibility_date = "2024-11-01"
# Development
[env.dev]
vars = { ENVIRONMENT = "development" }
# Production
[env.production]
vars = { ENVIRONMENT = "production" }
Environment Variables & Secrets
Access via the env parameter (NOT environment variables):
const apiKey = env.MAILGUN_API_KEY
const domain = env.MAILGUN_DOMAIN
Set secrets using wrangler CLI:
# For development
wrangler secret put MAILGUN_API_KEY --env dev
# For production
wrangler secret put MAILGUN_API_KEY --env production
For local development, create .dev.vars:
MAILGUN_API_KEY=key-xxxxx
MAILGUN_DOMAIN=mg.example.com
Never commit .dev.vars to git!
CORS Handling
Handle CORS in every worker (no shared file needed):
// Preflight
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'content-type, authorization',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
},
})
}
// Add to all responses
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
}
Calling External APIs
Use native fetch() - no libraries needed:
const response = await fetch('https://api.example.com/endpoint', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${env.API_KEY}`,
},
body: JSON.stringify(payload),
})
if (!response.ok) {
throw new Error(`API error: ${response.status} ${response.statusText}`)
}
const data = await response.json()
README Template
# Function Name
Brief description of what this does.
## External API
- **Service**: [API Name](https://docs-url.com)
- **Endpoint**: `POST /v1/endpoint`
- **What it does**: One sentence
## Setup
1. Get API key from [Service Dashboard](https://example.com)
2. Set secrets:
```bash
wrangler secret put API_KEY
- Deploy:
wrangler deploy
Usage
curl -X POST 'https://example-name.your-subdomain.workers.dev' \
-H 'Content-Type: application/json' \
-d '{
"param": "value"
}'
Response
{
"result": "success"
}
Local Development
# Install dependencies
npm install
# Run locally
wrangler dev
# Test
curl -X POST 'http://localhost:8787' \
-H 'Content-Type: application/json' \
-d '{"test": "data"}'
## Testing Locally
Document this pattern in README:
```bash
# Run worker locally (loads .dev.vars automatically)
wrangler dev
# Test in another terminal
curl -X POST 'http://localhost:8787' \
-H 'Content-Type: application/json' \
-d '{"test": "data"}'
Common Patterns
FormData for APIs that require it (e.g., Mailgun):
const formData = new FormData()
formData.append('from', 'sender@example.com')
formData.append('to', 'recipient@example.com')
formData.append('subject', 'Hello')
formData.append('text', 'Message body')
const response = await fetch(`https://api.mailgun.net/v3/${env.MAILGUN_DOMAIN}/messages`, {
method: 'POST',
headers: {
'Authorization': `Basic ${btoa(`api:${env.MAILGUN_API_KEY}`)}`,
},
body: formData,
})
Webhook verification (e.g., Stripe):
// Note: You'll need to use Web Crypto API or a small library
const encoder = new TextEncoder()
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(env.WEBHOOK_SECRET),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
)
const signature = request.headers.get('stripe-signature')
// Verify signature using Web Crypto API
Handle different HTTP methods:
const { method } = request
if (method === 'GET') {
const url = new URL(request.url)
const email = url.searchParams.get('email')
// handle GET
}
if (method === 'POST') {
const body = await request.json()
// handle POST
}
TypeScript Setup
Generate types automatically:
wrangler types
This creates worker-configuration.d.ts with proper types for your env and runtime.
Add to tsconfig.json:
{
"compilerOptions": {
"types": ["@cloudflare/workers-types/2023-07-01"]
}
}
What NOT to Include
- Node.js APIs (use Web APIs only)
- Heavy npm packages (Workers have size limits)
- Synchronous blocking operations
- File system operations (Workers are stateless)
When Creating New Examples
- Check
specs/for requirements - Create folder:
cloudflare/example-name/ - Copy template above into
src/index.ts - Create minimal
wrangler.toml - Implement the specific API integration
- Create README with complete documentation
- Test locally with
wrangler dev - Keep it simple - one API call, clear pattern
Deployment
# Development
wrangler deploy --env dev
# Production
wrangler deploy --env production
Key Differences from Other Platforms
- No Deno: Use Web APIs, not Node.js or Deno
- Module exports: Use
export default { fetch() } - Env parameter: Secrets come from
env, not environment variables - No shared CORS file: Include CORS in each worker
- Size limits: Keep bundles small (<1MB)
- Stateless: No persistent memory between requests