Hono Framework Guide | Ultra-Fast Edge Web Framework

Hono Framework Guide | Ultra-Fast Edge Web Framework

이 글의 핵심

Hono is a tiny, fast web framework built on Web Standards — same code runs on Cloudflare Workers, Deno, Bun, and Node.js. This guide covers everything from routing to production JWT auth and edge database patterns.

Why Hono?

Hono is a lightweight, ultra-fast web framework built on Web Standards (Request/Response, Fetch API). It runs identically on:

  • Cloudflare Workers — edge, zero cold start
  • Deno / Deno Deploy
  • Bun
  • Node.js (via adapter)
  • AWS Lambda, Vercel Edge

The Radix Tree router delivers consistent performance at any scale. The entire framework is ~14KB gzipped.


Quick Start

# Cloudflare Workers project
npm create cloudflare@latest my-api -- --template hono

# Bun project
bun create hono my-api

# Node.js
npm install hono @hono/node-server

1. Routing

import { Hono } from 'hono'

const app = new Hono()

// Basic routes
app.get('/', (c) => c.text('Hello Hono!'))
app.post('/users', (c) => c.json({ created: true }))
app.put('/users/:id', (c) => c.json({ id: c.req.param('id') }))
app.delete('/users/:id', (c) => c.json({ deleted: true }))

// Route params
app.get('/users/:id', (c) => {
  const id = c.req.param('id')
  return c.json({ id })
})

// Query params
app.get('/search', (c) => {
  const q = c.req.query('q')
  const page = c.req.query('page') ?? '1'
  return c.json({ q, page })
})

// Multiple params
app.get('/posts/:year/:month', (c) => {
  const { year, month } = c.req.param()
  return c.json({ year, month })
})

export default app

Route groups

// Group routes under a prefix
const api = new Hono().basePath('/api')

const users = new Hono()
users.get('/', (c) => c.json({ users: [] }))
users.get('/:id', (c) => c.json({ id: c.req.param('id') }))
users.post('/', async (c) => {
  const body = await c.req.json()
  return c.json({ created: body }, 201)
})

api.route('/users', users)

const app = new Hono()
app.route('/', api)  // GET /api/users, GET /api/users/:id

export default app

2. Middleware

Hono middleware wraps request handling — logging, auth, validation.

import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { prettyJSON } from 'hono/pretty-json'

const app = new Hono()

// Built-in middleware
app.use('*', logger())
app.use('*', prettyJSON())
app.use('/api/*', cors({
  origin: ['https://myapp.com', 'https://staging.myapp.com'],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowHeaders: ['Content-Type', 'Authorization'],
}))

// Custom middleware
app.use('*', async (c, next) => {
  const start = Date.now()
  await next()
  const elapsed = Date.now() - start
  c.res.headers.set('X-Response-Time', `${elapsed}ms`)
})

// Route-specific middleware
const authMiddleware = async (c: Context, next: Next) => {
  const token = c.req.header('Authorization')?.replace('Bearer ', '')
  if (!token) return c.json({ error: 'Unauthorized' }, 401)
  // validate token...
  await next()
}

app.get('/protected', authMiddleware, (c) => c.json({ data: 'secret' }))

Context — passing data between middleware

import { createMiddleware } from 'hono/factory'

// Type-safe context variables
type Variables = {
  userId: string
  userRole: 'admin' | 'user'
}

const app = new Hono<{ Variables: Variables }>()

const auth = createMiddleware<{ Variables: Variables }>(async (c, next) => {
  const token = c.req.header('Authorization')?.replace('Bearer ', '')
  if (!token) return c.json({ error: 'Unauthorized' }, 401)

  // Decode token (simplified)
  const payload = decodeJWT(token)
  c.set('userId', payload.sub)
  c.set('userRole', payload.role)
  await next()
})

app.get('/profile', auth, (c) => {
  const userId = c.get('userId')   // type: string
  const role = c.get('userRole')  // type: 'admin' | 'user'
  return c.json({ userId, role })
})

3. JWT Authentication

npm install hono  # jwt middleware is built-in
import { jwt } from 'hono/jwt'
import { sign, verify } from 'hono/jwt'

const JWT_SECRET = process.env.JWT_SECRET!

// Protect routes
app.use('/api/*', jwt({ secret: JWT_SECRET }))

// Login — issue token
app.post('/auth/login', async (c) => {
  const { email, password } = await c.req.json()

  const user = await db.users.findUnique({ where: { email } })
  if (!user || !await bcrypt.compare(password, user.passwordHash)) {
    return c.json({ error: 'Invalid credentials' }, 401)
  }

  const token = await sign(
    { sub: user.id, role: user.role, exp: Math.floor(Date.now() / 1000) + 3600 },
    JWT_SECRET
  )

  return c.json({ token })
})

// Access JWT payload
app.get('/api/me', jwt({ secret: JWT_SECRET }), (c) => {
  const payload = c.get('jwtPayload')
  return c.json({ userId: payload.sub, role: payload.role })
})

4. Request Validation

Hono’s Zod validator provides type-safe request parsing:

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

const createUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  role: z.enum(['admin', 'user']).default('user'),
})

const updateUserSchema = createUserSchema.partial()

const querySchema = z.object({
  page: z.coerce.number().min(1).default(1),
  limit: z.coerce.number().min(1).max(100).default(20),
  search: z.string().optional(),
})

app.post(
  '/users',
  zValidator('json', createUserSchema),
  async (c) => {
    const body = c.req.valid('json')  // fully typed
    const user = await db.users.create({ data: body })
    return c.json(user, 201)
  }
)

app.get(
  '/users',
  zValidator('query', querySchema),
  async (c) => {
    const { page, limit, search } = c.req.valid('query')
    const users = await db.users.findMany({
      skip: (page - 1) * limit,
      take: limit,
      where: search ? { name: { contains: search } } : undefined,
    })
    return c.json({ users, page, limit })
  }
)

5. Cloudflare Workers — Full Setup

// src/index.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { jwt } from 'hono/jwt'

// Cloudflare Workers bindings type
type Bindings = {
  DB: D1Database        // D1 SQLite
  KV: KVNamespace       // KV store
  JWT_SECRET: string    // Secret from wrangler.toml
}

const app = new Hono<{ Bindings: Bindings }>()

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

// D1 database query
app.get('/posts', async (c) => {
  const { results } = await c.env.DB.prepare(
    'SELECT * FROM posts WHERE published = 1 ORDER BY created_at DESC LIMIT 20'
  ).all()

  return c.json({ posts: results })
})

app.get('/posts/:id', async (c) => {
  const id = c.req.param('id')
  const post = await c.env.DB.prepare(
    'SELECT * FROM posts WHERE id = ?'
  ).bind(id).first()

  if (!post) return c.json({ error: 'Not found' }, 404)
  return c.json(post)
})

app.post('/posts', jwt({ secret: (c) => c.env.JWT_SECRET }), async (c) => {
  const body = await c.req.json()
  const { success } = await c.env.DB.prepare(
    'INSERT INTO posts (title, content, created_at) VALUES (?, ?, ?)'
  ).bind(body.title, body.content, new Date().toISOString()).run()

  return c.json({ success }, 201)
})

// KV cache
app.get('/config', async (c) => {
  const cached = await c.env.KV.get('site-config', 'json')
  if (cached) return c.json(cached)

  const config = await fetchConfig()
  await c.env.KV.put('site-config', JSON.stringify(config), { expirationTtl: 3600 })
  return c.json(config)
})

export default app
# wrangler.toml
name = "my-api"
main = "src/index.ts"
compatibility_date = "2024-01-01"

[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "your-database-id"

[[kv_namespaces]]
binding = "KV"
id = "your-kv-id"

[vars]
JWT_SECRET = "your-secret"  # use wrangler secret put JWT_SECRET for production
# Deploy
wrangler deploy

# Run locally
wrangler dev

6. Error Handling

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

// Throw structured errors anywhere
app.get('/users/:id', async (c) => {
  const user = await db.users.findUnique({ where: { id: c.req.param('id') } })

  if (!user) {
    throw new HTTPException(404, { message: 'User not found' })
  }

  return c.json(user)
})

// Global error handler
app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return c.json({ error: err.message }, err.status)
  }

  console.error('Unhandled error:', err)
  return c.json({ error: 'Internal server error' }, 500)
})

// 404 handler
app.notFound((c) => c.json({ error: 'Route not found' }, 404))

7. Streaming Responses

Ideal for AI/LLM output:

import { streamText } from 'hono/streaming'

app.post('/chat', async (c) => {
  const { messages } = await c.req.json()

  return streamText(c, async (stream) => {
    const response = await openai.chat.completions.create({
      model: 'gpt-4',
      messages,
      stream: true,
    })

    for await (const chunk of response) {
      const content = chunk.choices[0]?.delta?.content
      if (content) {
        await stream.write(`data: ${JSON.stringify({ content })}\n\n`)
      }
    }

    await stream.write('data: [DONE]\n\n')
  })
})

8. Node.js Deployment

// server.ts
import { serve } from '@hono/node-server'
import app from './app'

serve({
  fetch: app.fetch,
  port: 3000,
}, (info) => {
  console.log(`Server running on http://localhost:${info.port}`)
})

Hono vs Express vs Fastify

HonoExpressFastify
RuntimeAny (edge + Node)Node.jsNode.js
SpeedFastestSlowestFast
Bundle size~14KB~57KB~180KB
TypeScriptFirst-classCommunity typesBuilt-in
Edge supportNativeVia adapterLimited
EcosystemGrowingMatureMature

Key Takeaways

  • Hono = Web Standards framework that runs everywhere — Workers, Deno, Bun, Node
  • Routing: Radix Tree-based, supports params, query, route groups
  • Middleware: built-in logger, CORS, JWT, rate limiting — plus custom middleware with typed context
  • Validation: @hono/zod-validator for type-safe request parsing
  • Cloudflare Workers: D1 (SQLite), KV, Secrets via wrangler.toml bindings
  • Error handling: HTTPException + app.onError() for consistent responses
  • Streaming: streamText() for AI response streaming