An API Gateway is a single entry point that sits in front of your backend services.
Every client request flows through it — the gateway handles cross-cutting concerns
(authentication, rate limiting, logging, routing)
so individual services don't have to. This one runs entirely on
Cloudflare Workers at the edge — no origin server, sub-millisecond cold starts.
Request Flow
Client
│ X-API-Key: demo-key-alpha
▼
Gateway Worker (api.zyxwonderland.xyz)
├─ 1. Auth check — valid key → keyed tier | no key → anon tier | bad key → 401
├─ 2. Rate limit — keyed: 120 req/min | anon: 20 req/min | over limit → 429
├─ 3. Route match — /api/hello, /api/time, /api/echo, /api/slow, /api/fail
├─ 4. Handler — generate response, measure latency
└─ 5. Log to DO — method, path, status, latency, key_id, timestamp
│
▼
AnalyticsDO (Durable Object — SQLite)
└─ /analytics/summary, /analytics/timeseries, /analytics/recent
API Routes
| Method | Path | Auth | Description |
| GET | /api/hello | Optional | Returns a greeting and timestamp |
| GET | /api/time | Optional | Returns current UTC time (ISO + Unix) |
| ANY | /api/echo | Optional | Reflects method, path, query params, and body |
| GET | /api/slow | Optional | Simulates 200–800ms upstream latency |
| GET | /api/fail | Optional | Always returns 500 (for testing error logging) |
| GET | /analytics/summary | None | Aggregated metrics for the last 24h |
| GET | /analytics/timeseries | None | Hourly request counts (last 24h) |
| GET | /analytics/recent | None | Last 20 logged requests |
Rate Limit Headers
Every API response includes X-RateLimit-Limit and X-RateLimit-Remaining.
Limits reset every 60 seconds. Authenticated requests (X-API-Key) get
120 req/min; anonymous requests get 20 req/min.
Exceeding the limit returns 429 Too Many Requests.
Demo API Keys
Two demo keys are configured: demo-key-alpha and demo-key-beta.
Use either in the X-API-Key header or in the Test Console above.
All requests — keyed or anonymous — are logged to the dashboard.
Data Model — AnalyticsDO SQLite Schema
Table: requests
id INTEGER PRIMARY KEY AUTOINCREMENT
method TEXT NOT NULL -- GET, POST, PUT, DELETE
path TEXT NOT NULL -- /api/hello, /api/echo, …
status INTEGER NOT NULL -- 200, 400, 429, 500, …
latency_ms INTEGER NOT NULL -- wall-clock handler time
key_id TEXT NOT NULL DEFAULT '' -- empty string = anonymous
ts INTEGER NOT NULL -- Unix ms
Indexes: (ts) · (path, ts) · (status, ts)
Rate-limit counters (DO KV storage, not SQLite)
Key: rl:{identity}:{minute-slot}
Value: request count in that minute window
Slot: Math.floor(Date.now() / 60000)
TTL: previous slot deleted on each write
Design Decisions
Why Durable Objects instead of Workers KV for analytics?
KV is eventually consistent and optimised for reads at the edge. Writing per-request metrics
to KV would be expensive and could return stale counts. A Durable Object serialises all
writes through a single instance with a SQLite database — reads and writes are always
consistent, and SQL aggregations (GROUP BY, COUNT, AVG) are fast.
Why per-minute rate limiting with DO storage?
Each rate-limit check is a single DO storage read + write. Using the current minute as a
slot key means counters reset automatically — no background cleanup job needed. The previous
slot is pruned on each write to keep storage bounded. The DO's single-instance guarantee
means two concurrent requests cannot both read "count = 0" and both increment to 1;
writes are serialised.
Fire-and-forget logging.
The gateway logs each request with stub.fetch(…) without awaiting the result.
This keeps response latency unaffected by the write path — the Worker returns the response
to the client immediately. The Cloudflare runtime keeps the Worker alive long enough to
complete the background fetch before the isolate is terminated.