Skip to content

Workers Core

Foundational patterns for writing Cloudflare Workers: entry points, bindings, request handling, and TypeScript setup.

Prerequisites: OVERVIEW.md


Cloudflare Workers are serverless functions that run at the edge. This document covers the fundamentals of writing Workers code—the patterns every z0 Worker uses.

ConceptDescription
WorkerA JavaScript/TypeScript module that handles requests
BindingConnection to another Cloudflare resource (DO, D1, R2, etc.)
EnvironmentObject containing bindings and secrets
HandlerFunction that responds to events (fetch, scheduled, queue)

src/index.ts
export interface Env {
// Bindings declared here
DB: D1Database;
ENTITIES: DurableObjectNamespace;
STORAGE: R2Bucket;
QUEUE: Queue;
// Secrets and variables
TWILIO_AUTH_TOKEN: string;
ENVIRONMENT: string;
}
export default {
// HTTP requests
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
return handleRequest(request, env, ctx);
},
// Cron triggers
async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise<void> {
await handleScheduled(event, env, ctx);
},
// Queue consumers
async queue(batch: MessageBatch, env: Env, ctx: ExecutionContext): Promise<void> {
await handleQueue(batch, env, ctx);
},
};
// Export Durable Object classes
export { EntityDO } from './durable-objects/entity';
export { LedgerDO } from './durable-objects/ledger';
name = "z0-api"
main = "src/index.ts"
compatibility_date = "2024-01-01"
# Environment variables
[vars]
ENVIRONMENT = "production"
# D1 Database
[[d1_databases]]
binding = "DB"
database_name = "z0-main"
database_id = "xxxx-xxxx-xxxx"
# Durable Objects
[durable_objects]
bindings = [
{ name = "ENTITIES", class_name = "EntityDO" },
{ name = "LEDGERS", class_name = "LedgerDO" }
]
[[migrations]]
tag = "v1"
new_classes = ["EntityDO", "LedgerDO"]
# R2 Bucket
[[r2_buckets]]
binding = "STORAGE"
bucket_name = "z0-storage"
# Queue
[[queues.producers]]
binding = "QUEUE"
queue = "z0-jobs"
[[queues.consumers]]
queue = "z0-jobs"
max_batch_size = 10
max_batch_timeout = 30
# Cron triggers
[triggers]
crons = ["0 * * * *"] # Every hour
# Secrets (set via wrangler secret put)
# TWILIO_AUTH_TOKEN
# STRIPE_SECRET_KEY

src/router.ts
type RouteHandler = (
request: Request,
env: Env,
ctx: ExecutionContext,
params: Record<string, string>
) => Promise<Response>;
interface Route {
method: string;
pattern: RegExp;
handler: RouteHandler;
}
const routes: Route[] = [
// Entities
{ method: 'GET', pattern: /^\/entities\/([^/]+)$/, handler: getEntity },
{ method: 'POST', pattern: /^\/entities$/, handler: createEntity },
{ method: 'PATCH', pattern: /^\/entities\/([^/]+)$/, handler: updateEntity },
// Facts
{ method: 'GET', pattern: /^\/entities\/([^/]+)\/facts$/, handler: listFacts },
{ method: 'POST', pattern: /^\/entities\/([^/]+)\/facts$/, handler: appendFact },
// Webhooks
{ method: 'POST', pattern: /^\/webhooks\/twilio\/voice$/, handler: twilioVoice },
{ method: 'POST', pattern: /^\/webhooks\/stripe$/, handler: stripeWebhook },
];
export async function handleRequest(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
const url = new URL(request.url);
const method = request.method;
const path = url.pathname;
// Find matching route
for (const route of routes) {
if (route.method !== method) continue;
const match = path.match(route.pattern);
if (match) {
const params: Record<string, string> = {};
match.slice(1).forEach((value, index) => {
params[`p${index}`] = value;
});
try {
return await route.handler(request, env, ctx, params);
} catch (error) {
return handleError(error);
}
}
}
return new Response('Not Found', { status: 404 });
}
// Parse JSON body with validation
async function parseJsonBody<T>(request: Request, schema: z.ZodSchema<T>): Promise<T> {
const contentType = request.headers.get('content-type');
if (!contentType?.includes('application/json')) {
throw new HttpError(400, 'Content-Type must be application/json');
}
let body: unknown;
try {
body = await request.json();
} catch {
throw new HttpError(400, 'Invalid JSON');
}
const result = schema.safeParse(body);
if (!result.success) {
throw new HttpError(400, 'Validation failed', {
errors: result.error.flatten().fieldErrors,
});
}
return result.data;
}
// Parse URL search params
function parseSearchParams(url: URL): Record<string, string> {
const params: Record<string, string> = {};
url.searchParams.forEach((value, key) => {
params[key] = value;
});
return params;
}
// Parse form data (for Twilio webhooks)
async function parseFormData(request: Request): Promise<Record<string, string>> {
const formData = await request.formData();
const data: Record<string, string> = {};
formData.forEach((value, key) => {
if (typeof value === 'string') {
data[key] = value;
}
});
return data;
}
src/response.ts
export function json<T>(data: T, status = 200, headers: Record<string, string> = {}): Response {
return new Response(JSON.stringify(data), {
status,
headers: {
'Content-Type': 'application/json',
...headers,
},
});
}
export function created<T>(data: T, location?: string): Response {
const headers: Record<string, string> = {};
if (location) {
headers['Location'] = location;
}
return json(data, 201, headers);
}
export function noContent(): Response {
return new Response(null, { status: 204 });
}
export function notFound(message = 'Not found'): Response {
return json({ error: message }, 404);
}
export function badRequest(message: string, details?: unknown): Response {
return json({ error: message, details }, 400);
}

src/errors.ts
export class HttpError extends Error {
constructor(
public status: number,
message: string,
public details?: unknown
) {
super(message);
this.name = 'HttpError';
}
}
export class ValidationError extends HttpError {
constructor(message: string, public errors: Record<string, string[]>) {
super(400, message, errors);
this.name = 'ValidationError';
}
}
export class NotFoundError extends HttpError {
constructor(resource: string, id: string) {
super(404, `${resource} not found: ${id}`);
this.name = 'NotFoundError';
}
}
export class UnauthorizedError extends HttpError {
constructor(message = 'Unauthorized') {
super(401, message);
this.name = 'UnauthorizedError';
}
}
export class ForbiddenError extends HttpError {
constructor(message = 'Forbidden') {
super(403, message);
this.name = 'ForbiddenError';
}
}
src/error-handler.ts
export function handleError(error: unknown): Response {
console.error('Error:', error);
if (error instanceof HttpError) {
return json(
{
error: error.message,
details: error.details,
},
error.status
);
}
// Don't leak internal errors
return json(
{ error: 'Internal server error' },
500
);
}
// For more detailed error responses
interface ProblemDetails {
type: string;
title: string;
status: number;
detail?: string;
instance?: string;
[key: string]: unknown;
}
export function problemResponse(problem: ProblemDetails): Response {
return new Response(JSON.stringify(problem), {
status: problem.status,
headers: {
'Content-Type': 'application/problem+json',
},
});
}
// Usage
return problemResponse({
type: 'https://web1.co/errors/budget-exceeded',
title: 'Budget Exceeded',
status: 402,
detail: 'Daily budget of $500 has been reached',
instance: `/accounts/${accountId}`,
budget_limit: 500,
budget_spent: 512.50,
});

src/auth.ts
interface AuthContext {
tenantId: string;
userId?: string;
scopes: string[];
}
export async function authenticate(
request: Request,
env: Env
): Promise<AuthContext> {
const authHeader = request.headers.get('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
throw new UnauthorizedError('Missing or invalid Authorization header');
}
const token = authHeader.slice(7);
// Look up API key
const apiKey = await env.DB.prepare(`
SELECT tenant_id, user_id, scopes, status
FROM api_keys
WHERE key_hash = ?
`).bind(hashApiKey(token)).first();
if (!apiKey || apiKey.status !== 'active') {
throw new UnauthorizedError('Invalid API key');
}
return {
tenantId: apiKey.tenant_id as string,
userId: apiKey.user_id as string | undefined,
scopes: JSON.parse(apiKey.scopes as string),
};
}
function hashApiKey(key: string): string {
// Use Web Crypto API
const encoder = new TextEncoder();
const data = encoder.encode(key);
return crypto.subtle.digest('SHA-256', data)
.then(hash => btoa(String.fromCharCode(...new Uint8Array(hash))));
}
export function requireScope(auth: AuthContext, scope: string): void {
if (!auth.scopes.includes(scope) && !auth.scopes.includes('*')) {
throw new ForbiddenError(`Missing required scope: ${scope}`);
}
}
// Usage in handler
async function createEntity(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
const auth = await authenticate(request, env);
requireScope(auth, 'entities:write');
// ... create entity
}
// Twilio signature validation
import { validateRequest } from 'twilio';
export async function validateTwilioWebhook(
request: Request,
env: Env
): Promise<boolean> {
const signature = request.headers.get('X-Twilio-Signature');
if (!signature) return false;
const url = request.url;
const params = Object.fromEntries(await request.clone().formData());
return validateRequest(
env.TWILIO_AUTH_TOKEN,
signature,
url,
params
);
}
// Stripe signature validation
import Stripe from 'stripe';
export async function validateStripeWebhook(
request: Request,
env: Env
): Promise<Stripe.Event> {
const signature = request.headers.get('stripe-signature');
if (!signature) {
throw new UnauthorizedError('Missing Stripe signature');
}
const body = await request.text();
const stripe = new Stripe(env.STRIPE_SECRET_KEY);
try {
return stripe.webhooks.constructEvent(
body,
signature,
env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
throw new UnauthorizedError('Invalid Stripe signature');
}
}

// Get DO stub by ID
async function getEntityDO(env: Env, entityId: string): Promise<DurableObjectStub> {
const id = env.ENTITIES.idFromName(entityId);
return env.ENTITIES.get(id);
}
// Call DO method
async function appendFactToEntity(
env: Env,
entityId: string,
fact: Fact
): Promise<void> {
const stub = await getEntityDO(env, entityId);
const response = await stub.fetch('https://do/facts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(fact),
});
if (!response.ok) {
const error = await response.json();
throw new Error(`DO error: ${error.message}`);
}
}
// Query with parameters
async function getEntitiesByTenant(
env: Env,
tenantId: string,
type?: string
): Promise<Entity[]> {
let query = 'SELECT * FROM entities WHERE tenant_id = ?';
const params: unknown[] = [tenantId];
if (type) {
query += ' AND type = ?';
params.push(type);
}
query += ' ORDER BY created_at DESC LIMIT 100';
const result = await env.DB.prepare(query).bind(...params).all();
return result.results as Entity[];
}
// Batch operations
async function insertFacts(env: Env, facts: Fact[]): Promise<void> {
const stmt = env.DB.prepare(`
INSERT INTO facts (id, type, subtype, entity_id, timestamp, data)
VALUES (?, ?, ?, ?, ?, ?)
`);
const batch = facts.map(f =>
stmt.bind(f.id, f.type, f.subtype, f.entity_id, f.timestamp, JSON.stringify(f.data))
);
await env.DB.batch(batch);
}
// Store recording
async function storeRecording(
env: Env,
callId: string,
audio: ArrayBuffer
): Promise<string> {
const key = `recordings/${callId}.mp3`;
await env.STORAGE.put(key, audio, {
httpMetadata: {
contentType: 'audio/mpeg',
},
customMetadata: {
callId,
uploadedAt: new Date().toISOString(),
},
});
return key;
}
// Get signed URL for playback
async function getRecordingUrl(
env: Env,
key: string,
expiresIn = 3600
): Promise<string> {
const object = await env.STORAGE.get(key);
if (!object) {
throw new NotFoundError('Recording', key);
}
// R2 doesn't have native signed URLs, use presigned or proxy
return `https://api.web1.co/recordings/${encodeURIComponent(key)}`;
}
// Send message to queue
async function enqueueJob(
env: Env,
type: string,
payload: unknown
): Promise<void> {
await env.QUEUE.send({
type,
payload,
enqueuedAt: Date.now(),
});
}
// Send batch
async function enqueueBatch(
env: Env,
messages: Array<{ type: string; payload: unknown }>
): Promise<void> {
await env.QUEUE.sendBatch(
messages.map(m => ({
body: {
type: m.type,
payload: m.payload,
enqueuedAt: Date.now(),
},
}))
);
}
// Queue consumer handler
async function handleQueue(
batch: MessageBatch,
env: Env,
ctx: ExecutionContext
): Promise<void> {
for (const message of batch.messages) {
try {
const job = message.body as { type: string; payload: unknown };
switch (job.type) {
case 'transcribe':
await handleTranscription(env, job.payload);
break;
case 'analyze':
await handleAnalysis(env, job.payload);
break;
default:
console.warn(`Unknown job type: ${job.type}`);
}
message.ack();
} catch (error) {
console.error(`Job failed:`, error);
message.retry();
}
}
}

async function handleRequest(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
// Process request
const result = await processCall(request, env);
// Background work (doesn't block response)
ctx.waitUntil(async () => {
// Log to analytics
await logToAnalytics(env, result);
// Sync to external systems
await syncToCRM(env, result);
// Send notifications
await sendNotifications(env, result);
}());
// Return immediately
return json(result);
}
// Use Cache API for expensive operations
async function getCachedConfig(
env: Env,
ctx: ExecutionContext,
configId: string
): Promise<Config> {
const cache = caches.default;
const cacheKey = new Request(`https://cache/configs/${configId}`);
// Check cache
let response = await cache.match(cacheKey);
if (response) {
return response.json();
}
// Fetch from D1
const config = await getConfigFromD1(env, configId);
// Cache for 5 minutes
response = new Response(JSON.stringify(config), {
headers: {
'Cache-Control': 'max-age=300',
},
});
ctx.waitUntil(cache.put(cacheKey, response.clone()));
return config;
}

{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"types": ["@cloudflare/workers-types"],
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
{
"name": "z0-api",
"type": "module",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"test": "vitest",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240117.0",
"typescript": "^5.3.0",
"wrangler": "^3.22.0",
"vitest": "^1.2.0"
},
"dependencies": {
"zod": "^3.22.0"
}
}
src/types.ts
// Entity types
export interface Entity {
id: string;
type: EntityType;
subtype?: string;
name: string;
identifier?: string;
tenant_id: string;
owner_id?: string;
status: 'active' | 'inactive' | 'deleted';
metadata: Record<string, unknown>;
created_at: number;
updated_at: number;
}
export type EntityType =
| 'account'
| 'asset'
| 'tool'
| 'vendor'
| 'campaign'
| 'user'
| 'contact'
| 'deal';
// Fact types
export interface Fact {
id: string;
type: FactType;
subtype?: string;
timestamp: number;
entity_id: string;
tenant_id: string;
source_id?: string;
config_id?: string;
config_version?: number;
data: Record<string, unknown>;
}
export type FactType =
| 'invocation'
| 'outcome'
| 'charge'
| 'payout'
| 'cost'
| 'lifecycle'
| 'decision';
// Config types
export interface Config {
id: string;
type: ConfigType;
category: 'logic' | 'policy';
applies_to: string;
scope: 'platform' | 'tenant' | 'account' | 'campaign' | 'asset';
version: number;
settings: Record<string, unknown>;
created_at: number;
created_by: string;
}
export type ConfigType =
| 'pricing'
| 'qualification'
| 'routing'
| 'budget'
| 'hours'
| 'autonomy'
| 'webhook';

vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'miniflare',
environmentOptions: {
modules: true,
d1Databases: ['DB'],
durableObjects: {
ENTITIES: 'EntityDO',
},
},
},
});
src/handlers/entity.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { createEntity } from './entity';
describe('createEntity', () => {
let env: Env;
beforeEach(() => {
env = getMiniflareBindings();
});
it('creates an entity and returns 201', async () => {
const request = new Request('https://api/entities', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'account',
name: 'Test Account',
tenant_id: 'tenant_001',
}),
});
const response = await createEntity(request, env, {} as ExecutionContext, {});
expect(response.status).toBe(201);
const body = await response.json();
expect(body.id).toBeDefined();
expect(body.type).toBe('account');
expect(body.name).toBe('Test Account');
});
it('returns 400 for invalid type', async () => {
const request = new Request('https://api/entities', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'invalid',
name: 'Test',
}),
});
const response = await createEntity(request, env, {} as ExecutionContext, {});
expect(response.status).toBe(400);
});
});

// Avoid top-level await
// BAD
const config = await fetchConfig(); // Blocks cold start
// GOOD
let cachedConfig: Config | null = null;
async function getConfig(env: Env): Promise<Config> {
if (!cachedConfig) {
cachedConfig = await fetchConfig(env);
}
return cachedConfig;
}
// Prefer built-in APIs over npm packages
// BAD
import moment from 'moment'; // Large bundle
// GOOD
const date = new Date().toISOString();
// Use Web Crypto API
const hash = await crypto.subtle.digest('SHA-256', data);
// Use native fetch
const response = await fetch(url);
// Stream large responses
async function streamLargeResult(
env: Env,
query: string
): Promise<Response> {
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();
const encoder = new TextEncoder();
// Start streaming in background
(async () => {
try {
const cursor = await env.DB.prepare(query).raw();
writer.write(encoder.encode('['));
let first = true;
for await (const row of cursor) {
if (!first) writer.write(encoder.encode(','));
writer.write(encoder.encode(JSON.stringify(row)));
first = false;
}
writer.write(encoder.encode(']'));
} finally {
writer.close();
}
})();
return new Response(readable, {
headers: { 'Content-Type': 'application/json' },
});
}

CheckDescription
Input validationValidate all user input with Zod schemas
AuthenticationVerify API keys/tokens on every request
AuthorizationCheck scopes before operations
Tenant isolationAlways filter by tenant_id
Webhook validationVerify signatures (Twilio, Stripe)
Error messagesDon’t leak internal details
CORSConfigure allowed origins
Rate limitingUse Cloudflare rate limiting or DO-based
SecretsNever log or expose secrets
SQL injectionAlways use parameterized queries
const CORS_HEADERS = {
'Access-Control-Allow-Origin': 'https://app.web1.co',
'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400',
};
function handleOptions(): Response {
return new Response(null, { headers: CORS_HEADERS });
}
function addCorsHeaders(response: Response): Response {
const newHeaders = new Headers(response.headers);
Object.entries(CORS_HEADERS).forEach(([key, value]) => {
newHeaders.set(key, value);
});
return new Response(response.body, {
status: response.status,
headers: newHeaders,
});
}

ConceptImplementation
Entry pointsfetch, scheduled, queue handlers
Configurationwrangler.toml for bindings, triggers
Request handlingRouter pattern with typed handlers
Error handlingCustom error classes, RFC 7807
AuthenticationAPI keys, webhook signatures
BindingsDO, D1, R2, Queue via env object
Background workctx.waitUntil() for async tasks
TypeScriptES2022 target, workers-types
TestingVitest with miniflare
PerformanceMinimize cold start, stream large responses

This document covers the foundational patterns. See service-specific docs for Durable Objects, D1, Queues, etc.