Most "edge API" tutorials show you a hello-world handler and call it a day. That's not what production looks like. Production has auth, input validation, rate limiting, caching, observability, and tests. This post walks through how I build that on Cloudflare Workers using Hono.js.
I'll use a small URL-shortener API as the running example, but the patterns translate to anything CRUD-shaped.
Why Hono on Workers
Workers are tiny by design — V8 isolates with a 1 MB script limit and a few hundred MB of memory. Express won't run there. Hono is built for the edge: it's around 12 KB, has zero Node.js dependencies, ships first-class TypeScript types, and runs on Workers, Bun, Deno, Lambda, and Node.
The combination gives you APIs that:
- Cold-start in under 5ms (often sub-1ms).
- Run in 300+ Cloudflare data centers automatically.
- Cost effectively nothing under 10M req/month.
- Have proper TS inference end-to-end.
Project skeleton
Generate a project with the official template:
npm create hono@latest my-api -- --template cloudflare-workers
cd my-api
npm install
npm install @hono/zod-validator zod
Core middleware setup
The Worker entry mounts logging, secure headers, CORS, rate limiting, and the route module. Each middleware is one import and one app.use() line.
Auth: bearer tokens with KV-backed sessions
For a small service, JWT is overkill. Opaque tokens stored in KV are simpler and let you revoke instantly. Store the user ID under tok:<token>, look it up in the auth middleware, set it on the context. Token revocation is a single KV delete. No signature verification, no clock skew, no rotating secrets.
Input validation with Zod
Hono's zValidator middleware short-circuits with a 400 + machine-readable error for any invalid input. c.req.valid("json") is fully typed — change the schema and the handler types update.
Rate limiting
Cloudflare has a built-in rate-limit binding now, but the KV-based version still works everywhere and gives you per-route control. A sliding-bucket counter using KV with a 2x window TTL is close enough for almost every use case. For really high precision rate limiting, use Cloudflare's native RateLimit binding instead.
Caching
The redirect endpoint should be aggressively cached at the edge. Use the global caches.default API with the request URL as the cache key. c.executionCtx.waitUntil(cache.put(...)) writes the cache without blocking the response. A cached redirect serves in under 10ms globally. The KV miss path adds one read (~25ms). I've measured a p99 redirect latency of ~40ms across users from Asia, Europe, and North America.
Deploy
Configure two KV namespaces in wrangler.toml, set your secrets via wrangler secret put, then wrangler deploy. The API is now live on every Cloudflare edge node, globally.
Observability
Workers ship logs to the dashboard for free, but for anything serious I pipe to Axiom or use the Cloudflare Analytics Engine. A 5-line wrapper that POSTs path/status/ms to Axiom inside waitUntil works great. The waitUntil call is the trick — it lets the response return immediately while the log ships in the background.
Numbers from a real deployed service
- p50 latency: 22ms
- p99 latency: 74ms
- Cold start contribution: under 4ms
- Monthly cost at 4M requests: $0 (Workers free tier covers 10M/day)
- Total LOC including middleware and tests: under 800
When to skip Workers
This stack is a poor fit when:
- You need long-running connections (WebSockets are supported but with caveats — Durable Objects fix this).
- You need files larger than 100 MB in a single request.
- You depend on a Node-specific library that doesn't ship a Workers build.
- You need access to a real filesystem.
For everything else — small APIs, redirect services, lightweight backends, webhooks — this is the cheapest, fastest, most portable setup I've found.
If you want help shipping something similar, I take freelance work at m2hgamerz.prince@gmail.com.