How to Deploy a Hono API on Cloudflare Workers
Deploy a Hono API on Cloudflare Workers step by step: scaffold the project, add routes, middleware, and KV storage, test locally, and ship it worldwide.
If you want to ship a small API and have it run fast, everywhere, with almost no ops, it’s hard to beat Hono on Cloudflare Workers. Hono is a tiny, fast web framework — its API will feel instantly familiar if you’ve used Express — built to run on edge runtimes. Cloudflare Workers gives you serverless compute running in data centers around the world. Together they let you go from nothing to a globally deployed REST API in about ten minutes. Here’s the whole path, plus the middleware, validation, storage, and troubleshooting steps you’ll want right after “hello world.”
Prerequisites
- Node.js 20+ and npm.
- A Cloudflare account (the free tier is plenty for this).
Step 1: Scaffold the project
Hono ships an official starter that wires everything up for Workers:
npm create hono@latest my-api
When prompted for a template, choose cloudflare-workers. Then:
cd my-api
npm install
The generated project includes a wrangler.jsonc (your Workers config) and an entry file at src/index.ts.
Step 2: Read the starter
Open src/index.ts. The minimal app is just this:
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Hono!'))
export default app
c is the context — it holds the request and helpers for building the response. export default app is what hands your app to the Workers runtime.
Step 3: Build a small JSON API
Let’s add a couple of real routes — a health check and a dynamic path parameter:
app.get('/api/health', (c) => c.json({ status: 'ok' }))
app.get('/api/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ id, name: `User ${id}` })
})
app.post('/api/echo', async (c) => {
const body = await c.req.json()
return c.json({ youSent: body }, 201)
})
c.json() sets the content type and serializes for you; the second argument sets the status code. Path params come from c.req.param(), and c.req.json() parses a JSON body.
Step 4: Add middleware
Middleware in Hono is a function applied to a path pattern. The framework ships the common ones, so logging every request is one line:
import { logger } from 'hono/logger'
app.use('*', logger())
If a browser front end on another origin will call this API, you also need CORS. Scope it to your API routes and your real front-end origin rather than *:
import { cors } from 'hono/cors'
app.use('/api/*', cors({ origin: 'https://app.example.com' }))
Order matters: register middleware before the routes it should wrap. A request flows through each matching middleware, into your handler, and back out in reverse order.
Step 5: Validate request bodies
The POST /api/echo route above trusts whatever the client sends. For real endpoints, validate at the edge of the app. The idiomatic pairing is Zod plus Hono’s Zod validator:
npm install zod @hono/zod-validator
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
const createUser = z.object({
name: z.string().min(1),
email: z.string().email(),
})
app.post('/api/users', zValidator('json', createUser), (c) => {
const user = c.req.valid('json') // fully typed
return c.json({ created: user }, 201)
})
Bad payloads are rejected with a 400 before your handler runs, and c.req.valid('json') gives you a typed object — no as casts.
Step 6: Handle errors in one place
Two app-level hooks keep failure responses consistent JSON instead of default HTML:
app.notFound((c) => c.json({ error: 'route not found' }, 404))
app.onError((err, c) => {
console.error(err)
return c.json({ error: 'internal error' }, 500)
})
Step 7: Run it locally
npm run dev
That starts Wrangler’s local dev server (it emulates the Workers runtime, not Node) at http://localhost:8787, with hot reload on save. Hit your routes:
curl http://localhost:8787/api/health
# {"status":"ok"}
Because it’s the actual workerd runtime, what works here works in production — including bindings, which we’ll add next.
Step 8: Add storage with a KV binding
Workers connects to Cloudflare’s storage products through bindings declared in wrangler.jsonc and exposed on c.env. The simplest is Workers KV, a global key–value store. Create a namespace:
npx wrangler kv namespace create CACHE
Wrangler prints a config snippet — add it to wrangler.jsonc:
{
"kv_namespaces": [
{ "binding": "CACHE", "id": "<the id wrangler printed>" }
]
}
Then type the binding and use it in routes:
type Bindings = {
CACHE: KVNamespace
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/api/cache/:key', async (c) => {
const value = await c.env.CACHE.get(c.req.param('key'))
if (value === null) return c.json({ error: 'not found' }, 404)
return c.json({ value })
})
app.put('/api/cache/:key', async (c) => {
await c.env.CACHE.put(c.req.param('key'), await c.req.text())
return c.json({ stored: true })
})
Need relational queries instead? The same pattern works with D1, Cloudflare’s SQL database: declare a d1_databases binding and call c.env.DB.prepare(...). That edge-native data story is part of the broader rise of edge databases.
Step 9: Keep secrets out of your code
API keys and tokens don’t belong in wrangler.jsonc (it’s committed to git). Workers has a dedicated store for them:
npx wrangler secret put API_KEY
Wrangler prompts for the value and encrypts it server-side; it shows up on c.env.API_KEY just like a binding, so add it to your Bindings type as a string. For local development, put the same variable in a .dev.vars file (and add that file to .gitignore) — wrangler dev loads it automatically. Plain non-secret config, like a feature flag or an upstream URL, can live in wrangler.jsonc under "vars".
Step 10: Deploy worldwide
First, authenticate Wrangler with your Cloudflare account (one time):
npx wrangler login
Then ship it:
npm run deploy
Wrangler uploads your Worker and gives you a live *.workers.dev URL. That code is now running at Cloudflare’s edge — the same CDN footprint that serves their network — so requests are handled close to wherever your users are, with no servers for you to manage.
Step 11: Put it on a custom domain
The workers.dev URL is fine for testing, but a real API wants a real hostname. If your domain is on Cloudflare, it’s one block in wrangler.jsonc:
{
"routes": [
{ "pattern": "api.example.com", "custom_domain": true }
]
}
Redeploy, and Cloudflare provisions the DNS record and TLS certificate for you. No certificate renewal, no load balancer to configure.
Troubleshooting
wrangler logincan’t open a browser (SSH box, CI): create an API token in the Cloudflare dashboard and set it as theCLOUDFLARE_API_TOKENenvironment variable instead.c.env.CACHEis undefined: thebindingname inwrangler.jsoncmust match your code exactly, and the dev server needs a restart after config changes.- A Node library fails with missing
node:modules: Workers is not Node. Add"compatibility_flags": ["nodejs_compat"]towrangler.jsoncfor the supported Node APIs, or prefer Web-standard APIs (fetch, Web Crypto) — Hono itself uses only those. - “Worker threw exception” (error 1101) in production: run
npx wrangler tailto stream live logs from the deployed Worker and see the actual stack trace. - Worried about CPU limits: the free plan allows 10 ms of CPU per request — plenty for JSON APIs, since time spent awaiting KV or
fetchdoesn’t count. Heavy computation is the thing to offload.
Going further
To make deploys automatic on every push, wire wrangler deploy into a pipeline — our guide to CI/CD with GitHub Actions shows the pattern, and if you’ve deployed a site to Cloudflare Pages before, this will feel like the API-shaped sibling. Cloudflare keeps expanding what Workers can reach, as we covered in the developer platform’s expansion. For the full reference, the Cloudflare Workers docs and the Wrangler docs are the places to go.
The takeaway
Hono gives you an Express-familiar API that runs natively on the Workers runtime, and Wrangler turns deployment into one command. Scaffold with the cloudflare-workers template, add logging and CORS middleware, validate bodies with Zod, keep state in KV or D1 through typed bindings, and put the result on a custom domain. The whole stack stays in one config file and deploys globally in seconds — about as little ops as an API can have.
Tagged
Keep reading
Chisato · · 4 min read Cloudflare Keeps Pushing Compute to the Edge
Cloudflare's developer platform keeps growing — databases, object storage, and full-stack frameworks at the edge. What it means for how we ship web apps.
The Lycoris Team · · 4 min read What Is Serverless? Functions, Scaling, and Cost
Serverless means deploying code without managing servers — the platform scales it and you pay per use. How it works and where it fits.
Kurumi · · 6 min read Nebius Q2 2026 Earnings: Revenue Up 454%, Stock Soars
Nebius Q2 2026 revenue surged 454% to $582M and adjusted EBITDA turned positive as ARR hit $3B, sending NBIS up 34%. The neocloud numbers that mattered.