Contents

Hono Best Practices

Quick Start

Installation & Version

Hono is a small, simple, and ultrafast web framework built on Web Standards. It works on any JavaScript runtime.

# npm
npm create hono@latest

# yarn
yarn create hono

# pnpm
pnpm create hono@latest

# bun
bun create hono@latest

# deno
deno init
deno add npm:hono

Minimal Example

import { Hono } from 'hono'

const app = new Hono()

app.get('/', (c) => c.text('Hello Hono!'))

export default app

Core Concepts

Ultrafast Routing

Hono uses RegExpRouter - no linear loops. Routes are matched using regular expressions, making it extremely fast.

import { Hono } from 'hono'

const app = new Hono()

// Basic route
app.get('/users', (c) => c.json({ users: [] }))

// Path parameters (fully typed)
app.get('/users/:id', (c) => {
  const { id } = c.req.param()
  return c.json({ user: { id } })
})

// Optional parameters
app.get('/posts/:slug?', (c) => {
  const { slug } = c.req.param()
  return c.json({ slug: slug ?? 'default' })
})

// Regex constraints
app.get('/items/:date{[0-9]+}', (c) => {
  return c.text('Date pattern matched')
})

Context Object

The Context object (c) is instantiated for each request and provides all necessary utilities:

app.get('/example', (c) => {
  // Request access
  const method = c.req.method
  const path = c.req.path
  const query = c.req.query('name')
  const header = c.req.header('Authorization')

  // Response helpers
  return c.json({ message: 'OK' }, 200)
  // Or: c.text(), c.html(), c.body()

  // Set status
  c.status(201)

  // Set headers
  c.header('X-Custom', 'value')

  // Not found response
  return c.notFound()
})

Middleware Patterns

Middleware Execution Order

Middleware executes strictly in registration order. They wrap like onion layers:

middleware 1 start
  middleware 2 start
    handler
  middleware 2 end
middleware 1 end
import { logger } from 'hono/logger'

app.use(logger())  // runs first for all routes
app.use(authenticate())  // runs second

app.get('/foo', (c) => c.text('foo'))

Path-Specific Middleware

// Apply to specific path
app.use('/api/*', cors())

// Apply to specific method + path
app.post('/users/*', basicAuth())

Custom Middleware with Type Safety

Use createMiddleware() for reusable, typed middleware:

import { createMiddleware } from 'hono/factory'

const logger = createMiddleware(async (c, next) => {
  console.log(`[${c.req.method}] ${c.req.url}`)
  await next()
})

// Typed variables
const authMiddleware = createMiddleware<{
  Variables: { user: { id: string; name: string } }
}>(async (c, next) => {
  const user = await verifyToken(c)
  c.set('user', user)
  await next()
})

// Usage with automatic type merging
app
  .use(authMiddleware)
  .get('/', (c) => {
    const user = c.var.user  // fully typed
    return c.json({ name: user.name })
  })

Modifying Response After Handler

const addTimestamp = createMiddleware(async (c, next) => {
  await next()
  const originalText = await c.res.text()
  c.res = new Response(`[${Date.now()}] ${originalText}`)
})

Routing Architecture

Scaling with app.route()

For larger applications, create separate route files and mount them:

// routes/users.ts
import { Hono } from 'hono'
export const users = new Hono()

users.get('/', (c) => c.json({ users: [] }))
users.post('/', (c) => c.json({ id: '1' }, 201))

// routes/books.ts
import { Hono } from 'hono'
export const books = new Hono()

books.get('/', (c) => c.json({ books: [] }))

// index.ts
import { Hono } from 'hono'
import { users } from './routes/users'
import { books } from './routes/books'

const app = new Hono()

app.route('/api/users', users)
app.route('/api/books', books)

export default app

Base Path

const api = new Hono().basePath('/api')

api.get('/posts', (c) => c.json({ posts: [] }))

app.route('/api', api)

Type-Safe APIs with RPC

Server Setup

Export your app’s type for client inference:

import { Hono } from 'hono'
import { cors } from 'hono/cors'

const app = new Hono()

app.use('/api/*', cors())

app.get('/api/posts', (c) => {
  return c.json([{ id: 1, title: 'Hello' }])
})

app.post('/api/posts', async (c) => {
  const { title } = await c.req.json()
  return c.json({ id: 2, title }, 201)
})

export type AppType = typeof app
export default app

Client Usage

import { hc } from 'hono/client'

const client = hc<AppType>('http://localhost:8787/')

// Fully typed request/response
const res = await client.api.posts.$get()
const posts = await res.json()

// POST with typed body
const newPost = await client.api.posts.$post({
  json: { title: 'New Post' }
})

Status Code Typing

For explicit status codes in type inference:

// Good - status code is typed
app.get('/posts/:id', (c) => {
  const post = findPost(c.req.param('id'))
  if (!post) {
    return c.json({ error: 'Not found' }, 404)  // typed as 404
  }
  return c.json(post)  // typed as 200
})

// Alternative using HTTPException
app.get('/posts/:id', (c) => {
  const post = findPost(c.req.param('id'))
  if (!post) {
    throw new HTTPException(404, { message: 'Not found' })
  }
  return c.json(post)
})

Validation with Zod

Using @hono/zod-validator

import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

const postSchema = z.object({
  title: z.string().min(1),
  body: z.string().optional(),
})

const route = app.post(
  '/posts',
  zValidator('json', postSchema),
  (c) => {
    const { title, body } = c.req.valid('json')
    return c.json({ id: crypto.randomUUID(), title, body }, 201)
  }
)

export type RouteType = typeof route

Using @hono/standard-validator

Supports Zod, Valibot, and ArkType with a unified API:

import { sValidator } from '@hono/standard-validator'
import { z } from 'zod'

const schema = z.object({
  name: z.string(),
  age: z.number(),
})

app.post('/author', sValidator('json', schema), (c) => {
  const data = c.req.valid('json')
  return c.json({ success: true, message: `${data.name} is ${data.age}` })
})

Error Handling

HTTPException

import { HTTPException } from 'hono/http-exception'

app.post('/users', async (c) => {
  const body = await c.req.json()

  if (!body.email) {
    throw new HTTPException(400, { message: 'Email is required' })
  }

  const user = await createUser(body)
  return c.json(user, 201)
})

// Global error handler
app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return err.getResponse()
  }
  console.error(`${err}`)
  return c.text('Internal Server Error', 500)
})

// 404 handler
app.notFound((c) => {
  return c.json({ error: 'Not Found' }, 404)
})

Built-in Middleware

Hono includes comprehensive built-in middleware:

import {
  cors,
  logger,
  basicAuth,
  bearerAuth,
  jwt,
  compress,
  poweredBy,
  methodOverride,
} from 'hono/middleware'

Common Middleware Usage

import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { jwt } from 'hono/jwt'
import { compress } from 'hono/compress'

const app = new Hono()

app.use(logger())
app.use(compress())
app.use(cors({
  origin: ['https://example.com'],
  credentials: true,
}))

// JWT Authentication
app.use('/api/*', jwt({
  secret: process.env.JWT_SECRET,
}))

// Basic Auth
app.use('/admin/*', basicAuth({
  username: 'admin',
  password: process.env.ADMIN_PASSWORD,
}))

Multi-Runtime Deployment

Hono works seamlessly across different runtimes:

Cloudflare Workers

import { Hono } from 'hono'
import { cors } from 'hono/cors'

const app = new Hono()

app.use('/*', cors())

app.get('/hello', (c) => {
  return c.json({
    message: 'Hello from Cloudflare Workers!',
    env: c.env.ASSETS,  // Cloudflare binding
  })
})

export default {
  fetch: app.fetch,
  scheduled: app.scheduled,
}

Deno

import { Hono } from 'https://deno.land/x/hono/mod.ts'

const app = new Hono()
app.get('/', (c) => c.text('Hello from Deno!'))

Deno.serve(app.fetch)

Bun

import { Hono } from 'hono'

const app = new Hono()
app.get('/', (c) => c.text('Hello from Bun!'))

export default {
  port: 3000,
  fetch: app.fetch,
}

Node.js (with Adapter)

import { Hono } from 'hono'
import { serve } from '@hono/node-server'
import { cors } from 'hono/cors'

const app = new Hono()
app.use('/*', cors())

app.get('/', (c) => c.text('Hello from Node.js!'))

serve({
  fetch: app.fetch,
  port: 3000,
})

Performance Tips

Use Presets Appropriately

Hono offers different presets balancing size and features:

// hono/tiny - smallest, ~14KB
import { Hono } from 'hono/tiny'

// hono/base - includes more features
import { Hono } from 'hono/base'

// hono - full-featured (default)
import { Hono } from 'hono'

Avoid Controller-like Patterns

// Good - proper type inference
app.get('/users/:id', (c) => {
  const { id } = c.req.param()
  return c.json({ id })
})

// Avoid - loses type inference for path parameters
const getUser = (c) => c.json({ id: c.req.param('id') })
app.get('/users/:id', controller(getUser))

Use Factory Pattern When Needed

If you prefer organized controllers, use factory.createHandlers():

import { createFactory } from 'hono/factory'

const factory = createFactory()

const handlers = factory.createHandlers(
  logger(),
  authenticate(),
  (c) => {
    return c.json(c.var.user)
  }
)

app.get('/profile', ...handlers)

Testing

Using app.request()

describe('API', () => {
  it('GET /posts returns posts list', async () => {
    const res = await app.request('/posts')
    expect(res.status).toBe(200)
    const body = await res.json()
    expect(body.posts).toBeDefined()
  })

  it('POST /posts creates new post', async () => {
    const res = await app.request('/posts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title: 'Test' }),
    })
    expect(res.status).toBe(201)
  })

  it('Mock environment variables', async () => {
    const res = await app.request('/kv/test', {}, {
     Bindings: { MY_KV: { get: () => 'value' } },
    })
    expect(res.status).toBe(200)
  })
})

Project Structure Recommendation

src/
├── index.ts              # Main entry
├── routes/
│   ├── users.ts         # User routes
│   ├── posts.ts         # Post routes
│   └── api/
│       └── v1.ts        # API v1
├── middleware/
│   ├── auth.ts
│   └── logger.ts
├── lib/
│   └── db.ts
└── types/
    └── index.ts         # Shared types

Summary

Hono provides a lightweight yet powerful web framework with:

  • Ultrafast routing via RegExpRouter
  • Full TypeScript support with RPC type inference
  • Middleware composability with predictable execution order
  • Multi-runtime support (Cloudflare, Deno, Bun, Node.js, Lambda)
  • Built-in middleware for common tasks (CORS, Auth, Logging)
  • Zod validation integration for type-safe input

The key to effective Hono development: leverage TypeScript types, compose middleware wisely, and use app.route() for scalable architecture.