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
| Hono | Express | Fastify | |
|---|---|---|---|
| Runtime | Any (edge + Node) | Node.js | Node.js |
| Speed | Fastest | Slowest | Fast |
| Bundle size | ~14KB | ~57KB | ~180KB |
| TypeScript | First-class | Community types | Built-in |
| Edge support | Native | Via adapter | Limited |
| Ecosystem | Growing | Mature | Mature |
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-validatorfor type-safe request parsing - Cloudflare Workers: D1 (SQLite), KV, Secrets via
wrangler.tomlbindings - Error handling:
HTTPException+app.onError()for consistent responses - Streaming:
streamText()for AI response streaming