Skip to main content

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:

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

When to skip Workers

This stack is a poor fit when:

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.