---
title: Building APIs with Hono
date: '2026-02-10'
description: 'How I build type-safe APIs with Hono: the router, the Context object, Zod validation, centralized error handling, and end-to-end types shared with the frontend.'
author: Pablo Fernandez
tags:
  - Hono
  - APIs
  - TypeScript
  - Web Development
  - Bun
source: 'https://www.pablofr.com/blog/building-apis-with-hono'
---
[Hono](https://hono.dev) ("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](https://bun.sh), 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:

```bash
bun add hono
```

And a minimal app:

```typescript:index.ts {6}
import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => {
  return c.text('hello from Hono')
})

export default app
```

## The 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:

```typescript:api.ts /c.json/
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](https://zod.dev) straight into a route, so a bad body is rejected before your handler ever runs:

```typescript:server.ts {6,8,12} /zValidator/
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:

```typescript:rpc-example.ts
// 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:

```typescript:errors.ts {1,4} /onError/
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.
