Skip to content

API Gateway

Declarative HTTP routing with type-safe contracts, authentication, and rate limiting.

Prerequisites: YAML Manifests, Core Concepts


The z0 API Gateway layer enables you to define HTTP routes declaratively in your YAML manifest and have the SDK handle routing, authentication, validation, and response shaping automatically.

API Gateway is ideal for:

  • Contract-first development with type-safe oRPC contracts and OpenAPI generation
  • Declarative routing where routes are defined alongside entities and configs
  • Built-in middleware for auth, rate limiting, and validation
  • Consistent API responses with standard envelopes and RFC 7807 errors

The Gateway layer supports two approaches:

  1. GatewayWorker base class - Extend GatewayWorker to inherit routing and middleware
  2. Code generation - Generate complete Worker code from manifest using z0 generate gateway

Both approaches use the same manifest schema and provide identical functionality.

manifest.yaml
name: analytics-api
version: 1.0.0
routes:
- path: /v1/track
method: POST
entity: pageview
action: emit
factType: viewed
auth: api_key
rateLimit: { rpm: 1000, scope: tenant }
- path: /v1/sessions/:id
method: GET
entity: session
action: get
auth: api_key

Routes are defined in the routes section of your YAML manifest. Each route specifies the HTTP method, path, entity type, and action to perform.

routes:
- path: /v1/entities/:id # URL path (supports :param syntax)
method: GET # HTTP method: GET, POST, PUT, PATCH, DELETE
entity: account # Target entity type
action: get # Action: emit, get, create, list, query
factType: viewed # (Optional) Fact type for emit actions
auth: api_key # (Optional) Auth mode: api_key, public
rateLimit: # (Optional) Rate limiting config
rpm: 1000 # Requests per minute
scope: tenant # Scope: tenant, global
ActionMethodPurposeInputOutput
emitPOSTAppend a fact to an entity{ entityId, factType, data }{ factId, seq, timestamp }
getGETRetrieve an entity by ID{ entityId }Entity object
createPOSTCreate a new entity{ type, data }{ id, entity }
listGETList entities with pagination{ entityType, cursor?, limit? }{ items, cursor?, hasMore }
queryPOSTExecute a projection query{ projection, filters? }{ results, count }

Routes support path parameters using :paramName syntax:

routes:
- path: /v1/accounts/:accountId
method: GET
entity: account
action: get
- path: /v1/accounts/:accountId/transactions/:txId
method: GET
entity: transaction
action: get

Parameters are extracted from the URL and passed to the handler.


The GatewayWorker class provides a base implementation for API Gateway Workers. It automatically registers routes from your manifest and provides extensibility hooks.

import { GatewayWorker, type GatewayWorkerEnv } from '@z0-app/sdk';
import { parseManifest } from '@z0-app/sdk';
import manifestYaml from './manifest.yaml';
// Parse manifest
const { manifest } = parseManifest(manifestYaml);
// Extend GatewayWorker
export class MyGateway extends GatewayWorker<GatewayWorkerEnv> {
constructor(state: DurableObjectState, env: GatewayWorkerEnv) {
super(state, env, manifest);
}
}
// Export as Durable Object
export { MyGateway };

Add custom routes in your constructor after calling super():

export class MyGateway extends GatewayWorker<GatewayWorkerEnv> {
constructor(state: DurableObjectState, env: GatewayWorkerEnv) {
super(state, env, manifest);
// Add custom health check endpoint
this.router.get('/health', () => {
return Response.json({ status: 'ok', timestamp: Date.now() });
});
// Add custom analytics endpoint
this.router.post('/v1/batch', async (req) => {
const events = await req.json();
// Process batch events
return Response.json({ processed: events.length });
});
}
}

Override lifecycle hooks to add custom logic:

export class MyGateway extends GatewayWorker<GatewayWorkerEnv> {
/**
* Called before route handling
* Use for: logging, custom auth, request transformation
*/
protected async beforeRoute(request: Request): Promise<void> {
// Log incoming requests
console.log(`[${new Date().toISOString()}] ${request.method} ${new URL(request.url).pathname}`);
// Add custom validation
const contentType = request.headers.get('Content-Type');
if (request.method === 'POST' && !contentType?.includes('application/json')) {
throw new Error('Content-Type must be application/json');
}
}
/**
* Called after route handling
* Use for: response transformation, custom headers, logging
*/
protected async afterRoute(request: Request, response: Response): Promise<Response> {
// Clone response to add headers
const headers = new Headers(response.headers);
headers.set('X-API-Version', '1.0.0');
headers.set('X-Request-ID', crypto.randomUUID());
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers
});
}
}

The Gateway layer includes built-in middleware for common API concerns.

Two authentication modes are supported:

Requires a valid API key in the X-API-Key header:

routes:
- path: /v1/track
method: POST
entity: pageview
action: emit
auth: api_key # Validates X-API-Key header

The auth middleware uses the SDK’s authenticateRequest() function to validate keys. Keys must have the format {prefix}_{random} (e.g., z0_abc123).

Invalid requests return RFC 7807 errors:

{
"type": "https://docs.z0.app/errors/unauthorized",
"title": "Unauthorized",
"status": 401,
"detail": "Invalid or missing API key",
"instance": "/errors/550e8400-e29b-41d4-a716-446655440000"
}

Bypasses authentication for public endpoints:

routes:
- path: /health
method: GET
entity: system
action: get
auth: public # No authentication required

Per-route rate limiting prevents abuse and ensures fair resource usage:

routes:
- path: /v1/track
method: POST
entity: pageview
action: emit
rateLimit:
rpm: 1000 # Requests per minute
scope: tenant # Scope: tenant or global

Scope options:

  • tenant - Limit applies per tenant (identified by API key)
  • global - Limit applies across all requests to this route

Rate limit exceeded returns 429:

{
"type": "https://docs.z0.app/errors/rate-limit-exceeded",
"title": "Too Many Requests",
"status": 429,
"detail": "Rate limit exceeded: 1000 requests per minute",
"instance": "/errors/550e8400-e29b-41d4-a716-446655440000"
}

Response includes Retry-After header indicating seconds until limit resets.

Request bodies are automatically validated against Zod schemas defined in the oRPC contracts:

// For emit action
{
"entityId": "session_abc123",
"factType": "viewed",
"data": {
"url": "https://example.com/page",
"duration_ms": 5000
}
}

Invalid requests return 400 with field-level errors:

{
"type": "https://docs.z0.app/errors/validation-failed",
"title": "Validation Failed",
"status": 400,
"detail": "Request validation failed",
"errors": [
{
"field": "entityId",
"code": "required",
"message": "entityId is required"
}
],
"instance": "/errors/550e8400-e29b-41d4-a716-446655440000"
}

All responses follow a consistent envelope format.

Success responses use the { data, meta } envelope:

{
"data": {
"factId": "fact_abc123",
"seq": 42,
"timestamp": 1678901234567
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": 1678901234567
}
}

The meta object always includes:

  • request_id - Unique identifier for request tracing
  • timestamp - Unix timestamp (milliseconds) when response was generated

Error responses follow RFC 7807 Problem Details format:

{
"type": "https://docs.z0.app/errors/not-found",
"title": "Not Found",
"status": 404,
"detail": "Entity account_xyz not found",
"instance": "/errors/550e8400-e29b-41d4-a716-446655440000",
"trace_id": "550e8400-e29b-41d4-a716-446655440000"
}

RFC 7807 fields:

  • type - URI reference identifying the problem type
  • title - Short, human-readable summary
  • status - HTTP status code
  • detail - Explanation specific to this occurrence
  • instance - URI reference identifying this occurrence (includes request ID)
  • errors - (Optional) Array of field-level validation errors
  • trace_id - (Optional) Request ID for distributed tracing

Content-Type: application/problem+json


The Gateway uses oRPC contracts to define type-safe action interfaces with automatic validation.

import { gatewayContract } from '@z0-app/sdk';
// Contract includes all action types
export const gatewayContract = oc.router({
emit: oc
.input(z.object({
entityId: z.string(),
factType: z.string(),
data: z.record(z.unknown()),
}))
.output(z.object({
factId: z.string(),
seq: z.number(),
timestamp: z.number(),
})),
get: oc
.input(z.object({ entityId: z.string() }))
.output(EntitySchema),
// ... other actions
});
import type {
EmitRequest,
EmitResponse,
GetRequest,
GetResponse,
} from '@z0-app/sdk';
// Type-safe request
const request: EmitRequest = {
entityId: 'session_abc123',
factType: 'viewed',
data: { url: 'https://example.com' }
};
// Type-safe response
const response: EmitResponse = await client.emit(request);
console.log(response.factId); // TypeScript knows this exists

All contract types are exported from the SDK:

import type {
// Action request types
EmitRequest,
GetRequest,
CreateRequest,
ListRequest,
QueryRequest,
// Action response types
EmitResponse,
GetResponse,
CreateResponse,
ListResponse,
QueryResponse,
// Error types
ProblemDetails,
FieldError,
// SDK primitive types
Entity,
Fact,
} from '@z0-app/sdk';

Generate complete Worker code from your manifest using the z0 CLI.

Terminal window
npx z0 generate gateway --manifest manifest.yaml --output src/generated

Generated files:

  • src/generated/gateway.ts - Complete Worker implementation extending GatewayWorker

Generated code includes:

  • All routes from manifest registered with router
  • Auth middleware for routes with auth config
  • Rate limit middleware for routes with rateLimit config
  • Request validation for all actions
  • Response shaping with standard envelope
  • RFC 7807 error handling
  • Health check endpoint (/health)
src/index.ts
export { GeneratedGateway as default } from './generated/gateway';

Wrangler configuration:

wrangler.toml
name = "my-api-gateway"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[[durable_objects.bindings]]
name = "GATEWAY"
class_name = "GeneratedGateway"
script_name = "my-api-gateway"

Generate OpenAPI 3.0 specification from oRPC contracts:

Terminal window
npx z0 generate openapi --manifest manifest.yaml --output openapi.json

Generated spec includes:

  • All routes from manifest as OpenAPI paths
  • Zod schemas converted to JSON Schema
  • Request/response schemas for each action
  • Error response schemas (RFC 7807)
  • Authentication requirements (API key)

Use the generated spec for:

  • API documentation tools (Swagger UI, Redoc)
  • Client SDK generation (OpenAPI Generator)
  • Contract testing (Prism, Dredd)
  • Import into API management platforms

Validate your manifest before generation:

Terminal window
npx z0 check --manifest manifest.yaml

Returns exit code 0 for valid manifests, non-zero with error details for invalid manifests.


Here’s a complete example building an analytics API:

manifest.yaml
name: analytics-api
version: 1.0.0
entities:
pageview:
description: Single page view event
fields:
url: { type: string, indexed: true }
duration_ms: { type: number }
user_agent: { type: string }
facts:
- viewed
- engaged
session:
description: User session
fields:
started_at: { type: number }
last_active_at: { type: number }
page_count: { type: number }
facts:
- started
- ended
routes:
# Track pageview (append fact)
- path: /v1/track
method: POST
entity: pageview
action: emit
factType: viewed
auth: api_key
rateLimit: { rpm: 1000, scope: tenant }
# Get session (retrieve entity)
- path: /v1/sessions/:id
method: GET
entity: session
action: get
auth: api_key
# List pageviews (paginated)
- path: /v1/pageviews
method: GET
entity: pageview
action: list
auth: api_key
rateLimit: { rpm: 100, scope: tenant }
# Public health check
- path: /health
method: GET
entity: system
action: get
auth: public
src/gateway.ts
import { GatewayWorker, type GatewayWorkerEnv, parseManifest } from '@z0-app/sdk';
import manifestYaml from './manifest.yaml';
const { manifest } = parseManifest(manifestYaml);
export class AnalyticsGateway extends GatewayWorker<GatewayWorkerEnv> {
constructor(state: DurableObjectState, env: GatewayWorkerEnv) {
super(state, env, manifest);
// Add custom analytics endpoint
this.router.post('/v1/batch', async (req) => {
const events = await req.json();
// Process batch of events
const results = await Promise.all(
events.map(event => this.processBatchEvent(event))
);
return Response.json({
data: { processed: results.length, success: results.filter(r => r.ok).length },
meta: { request_id: crypto.randomUUID(), timestamp: Date.now() }
});
});
}
private async processBatchEvent(event: any): Promise<{ ok: boolean }> {
// Batch processing logic
return { ok: true };
}
protected async beforeRoute(request: Request): Promise<void> {
// Log all requests
const url = new URL(request.url);
console.log(`[Gateway] ${request.method} ${url.pathname}`);
}
protected async afterRoute(request: Request, response: Response): Promise<Response> {
// Add CORS headers
const headers = new Headers(response.headers);
headers.set('Access-Control-Allow-Origin', '*');
headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers
});
}
}
export { AnalyticsGateway as default };
wrangler.toml
name = "analytics-api"
main = "src/gateway.ts"
compatibility_date = "2024-01-01"
[[durable_objects.bindings]]
name = "ANALYTICS_GATEWAY"
class_name = "AnalyticsGateway"
script_name = "analytics-api"
[[d1_databases]]
binding = "DB"
database_name = "analytics"
database_id = "..."
Terminal window
npx wrangler deploy
Terminal window
# Track a pageview
curl -X POST https://analytics-api.your-workers.dev/v1/track \
-H "X-API-Key: YOUR_API_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{
"entityId": "pv_abc123",
"factType": "viewed",
"data": {
"url": "https://example.com/page",
"duration_ms": 5000,
"user_agent": "Mozilla/5.0..."
}
}'
# Response:
{
"data": {
"factId": "fact_xyz789",
"seq": 1,
"timestamp": 1678901234567
},
"meta": {
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"timestamp": 1678901234567
}
}
# Get a session
curl https://analytics-api.your-workers.dev/v1/sessions/session_123 \
-H "X-API-Key: YOUR_API_KEY_HERE"
# Response:
{
"data": {
"id": "session_123",
"type": "session",
"version": 3,
"data": {
"started_at": 1678900000000,
"last_active_at": 1678901234567,
"page_count": 5
},
"created_at": 1678900000000,
"updated_at": 1678901234567
},
"meta": {
"request_id": "660e8400-e29b-41d4-a716-446655440001",
"timestamp": 1678901234567
}
}

ComponentPurpose
GatewayWorkerBase class for API Gateway Workers with automatic routing
Routes schemaDeclarative route definitions in YAML manifest
Auth middlewareAPI key validation with public route support
Rate limit middlewarePer-route rate limiting (tenant/global scopes)
Validation middlewareAutomatic request validation via Zod schemas
Response shapingStandard envelope ({ data, meta }) for success responses
RFC 7807 errorsProblem Details format with request ID tracing
oRPC contractsType-safe action interfaces with automatic validation
Code generationGenerate complete Workers from manifests
OpenAPI generationAuto-generate OpenAPI 3.0 specs from contracts