Building APIs with Hono
Hono ("flame" in Japanese) is a small web framework for TypeScript. Where older frameworks assume a long-running Node.js server, Hono targets the runtimes I actually deploy to: Bun, Deno, Cloudflare Workers. It is a few kilobytes with no dependencies, and the API is close enough to Express that you can read it on sight.
This is how I build an API with it, and the part I like most is at the end: the types reach all the way to the frontend.
Why I reach for it#
Coming from Express, the shape feels familiar. A few things are different in ways that matter:
- The router is fast. Routes compile into a RegExp-based trie, so matching stays cheap as the route table grows.
- It is tiny. Zero dependencies and a small footprint, which is what keeps cold starts low on serverless.
- It runs where I deploy. The same code runs on Bun, Deno, Cloudflare Workers, and plain Node.js.
- The types are real. Hono is written in TypeScript, so the editor catches most mistakes before anything runs.
A first route#
With Bun, setup is one install:
bun add honoAnd a minimal app:
import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('hello from Hono')
})
export default appThe Context object#
Every handler is given c, the Context. It carries the request and the response, environment bindings, and any per-request state you set. Most of what you do in Hono goes through it:
app.get('/api/info', (c) => {
return c.json({
message: 'JSON in one call',
runtime: 'Bun',
timestamp: Date.now()
})
})Middleware and validation#
Hono ships logger and cors, and its zValidator wires Zod straight into a route, so a bad body is rejected before your handler ever runs:
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
const app = new Hono()
app.use('*', logger())
app.use('/api/*', cors())
const userSchema = z.object({
id: z.string(),
username: z.string().min(3),
email: z.string().email()
})
app.post('/api/users', zValidator('json', userSchema), (c) => {
const user = c.req.valid('json')
return c.json({ status: 'created', user }, 201)
})Inside the handler, c.req.valid('json') is already typed and already validated. You never re-check the shape.
End-to-end types#
This is the part I keep coming back to. You export the type of your routes from the server and hand it to the client, and the frontend gets autocompletion and type-checking across the network boundary with no code generation step:
// On the server
const routes = app.post('/posts', zValidator('json', postSchema), (c) => {
// ...
})
export type AppType = typeof routes
// On the client
import { hc } from 'hono/client'
const client = hc<AppType>('https://api.example.com')
// The request is type-checked against the server's route.
const res = await client.posts.$post({
json: { title: 'Building with Hono' }
})Change the server's schema and the client stops compiling in the right place. That feedback loop is the reason I picked it.
Error handling#
A single onError handler catches anything a route throws, so the error shape lives in one place instead of being repeated in every try/catch:
app.onError((err, c) => {
console.error(`[API Error]: ${err.message}`)
return c.json({
error: 'Internal Server Error',
message: err.message
}, 500)
})Where it fits#
What keeps me on Hono is not the speed, though it is fast. It is that the same small API runs on Bun in development and on an edge runtime in production, and that the types carry all the way to the client. There is little of it to learn and almost nothing to fight, which is exactly what I want from the layer that sits between a request and my code.
Related articles
whatsapp-web.js runs a whole headless Chromium to act like a phone. The other door skips the browser and speaks WhatsApp's native protocol over a plain WebSocket, then hands you a raw socket and a firehose of events. whatsweb is the thin layer I built to make that socket feel like a bot.
How I'm building Protomarc, a system that marks images, video, audio, and PDF so a file can prove who owns it and, if it leaks, point back to whoever it was given to. A high-level look at the problem and the hard parts.