Hono 프레임워크
왜 Hono인가
Workers의 기본 fetch 핸들러는 라우팅이 없다. /users, /posts, /health 같은 경로를 직접 if (url.pathname === '/users') 로 분기하면 파일이 금세 수백 줄이 된다. Hono는 이 문제를 풀어주는 초경량 웹 프레임워크다.
- 번들 크기 약 12 kB — Workers 메모리 한도(128 MB) 걱정 없음
- Express와 유사한 API — 러닝 커브가 낮음
- Workers / Node.js / Deno / Bun 모두 동작 — 한 코드베이스로 런타임 전환 가능
- TypeScript 지원이 1급 — 제네릭으로 요청/응답 타입 보장
# 새 Workers 프로젝트에 Hono 추가
npm create cloudflare@latest my-api -- --template=hono
# 또는 기존 프로젝트에 설치
npm install hono
기본 라우팅
// src/index.ts
import { Hono } from 'hono'
const app = new Hono()
// GET /
app.get('/', (c) => c.text('Hello from Hono!'))
// GET /users/:id — 경로 파라미터
app.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ userId: id })
})
// POST /users
app.post('/users', async (c) => {
const body = await c.req.json()
return c.json({ created: body }, 201)
})
// PUT / DELETE 도 동일 패턴
app.delete('/users/:id', (c) => {
return c.json({ deleted: c.req.param('id') })
})
export default app
c 는 Context 객체다. 요청 파싱과 응답 생성이 여기에 모여 있다.
c.req.* | 의미 |
|---|---|
c.req.param('id') | URL 경로 파라미터 |
c.req.query('page') | 쿼리스트링 |
c.req.json() | JSON 바디 파싱 (async) |
c.req.header('Authorization') | 요청 헤더 |
c.* 응답 메서드 | 결과 |
|---|---|
c.text('hello') | text/plain |
c.json({ ok: true }) | application/json |
c.html('<h1>Hi</h1>') | text/html |
c.redirect('/login') | 302 리다이렉트 |
라우터 그룹 & 중첩
규모가 커지면 라우트를 파일별로 분리한다.
// src/routes/users.ts
import { Hono } from 'hono'
const users = new Hono()
users.get('/', (c) => c.json({ users: [] }))
users.post('/', async (c) => { /* 생성 로직 */ })
users.get('/:id', (c) => c.json({ id: c.req.param('id') }))
export default users
// src/index.ts
import { Hono } from 'hono'
import users from './routes/users'
import posts from './routes/posts'
const app = new Hono()
// /api/v1/users/* 로 마운트
app.route('/api/v1/users', users)
app.route('/api/v1/posts', posts)
export default app
app.route('/prefix', subApp) 는 Express의 router.use('/prefix', router) 와 같다.
미들웨어
미들웨어는 next() 를 호출하는 함수다. 인증, 로깅, CORS 등을 핸들러 앞에 끼워넣는다.
글로벌 미들웨어
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
const app = new Hono()
// 모든 요청에 적용
app.use('*', logger())
app.use('*', cors({ origin: 'https://my-frontend.pages.dev' }))
직접 만드는 미들웨어 — JWT 인증 예시
import { createMiddleware } from 'hono/factory'
const authMiddleware = createMiddleware(async (c, next) => {
const token = c.req.header('Authorization')?.replace('Bearer ', '')
if (!token || token !== c.env.API_SECRET) {
return c.json({ error: 'Unauthorized' }, 401)
}
// 다음 핸들러로 진행
await next()
})
// 특정 경로에만 적용
app.use('/api/*', authMiddleware)
미들웨어 실행 순서
요청 → 미들웨어1 → 미들웨어2 → 핸들러 → 미들웨어2 (next 이후) → 미들웨어1 (next 이후) → 응답
await next() 앞은 전처리, await next() 뒤는 후처리다.
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`)
})
유효성 검사 — Zod 연동
Hono는 Zod Validator 미들웨어를 공식 지원한다.
npm install zod @hono/zod-validator
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const createUserSchema = z.object({
name: z.string().min(1).max(50),
email: z.string().email(),
age: z.number().int().min(0).max(120).optional(),
})
app.post(
'/users',
zValidator('json', createUserSchema), // ← 검사 실패 시 자동 400
async (c) => {
const data = c.req.valid('json') // ← 타입 추론 완료
// data.name, data.email 모두 string 타입 보장
return c.json({ created: data }, 201)
}
)
쿼리스트링도 동일하게 검사할 수 있다.
const listQuerySchema = z.object({
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
})
app.get('/users', zValidator('query', listQuerySchema), (c) => {
const { page, limit } = c.req.valid('query')
return c.json({ page, limit })
})
흔한 실수 —
z.number()는 쿼리스트링에 그대로 쓰면 항상 실패한다. 쿼리스트링은 문자열이므로 반드시z.coerce.number()를 써야 한다.
Workers 바인딩과 Hono 연동
D1, KV, R2 등 Workers 바인딩은 c.env 로 접근한다. 타입을 명시하면 자동 완성이 된다.
// src/index.ts
type Bindings = {
DB: D1Database
KV: KVNamespace
API_SECRET: string
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/posts', async (c) => {
const { results } = await c.env.DB
.prepare('SELECT * FROM posts ORDER BY created_at DESC LIMIT 20')
.all()
return c.json(results)
})
에러 핸들링
// 전역 에러 핸들러
app.onError((err, c) => {
console.error(err)
return c.json({ error: 'Internal Server Error' }, 500)
})
// 404 핸들러
app.notFound((c) => {
return c.json({ error: `${c.req.path} not found` }, 404)
})
실전: 미니 블로그 API 전체 예시
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
type Bindings = { DB: D1Database }
const app = new Hono<{ Bindings: Bindings }>()
app.use('*', logger())
app.use('*', cors())
const postSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1),
})
// 목록 조회
app.get('/posts', async (c) => {
const { results } = await c.env.DB
.prepare('SELECT id, title, created_at FROM posts ORDER BY created_at DESC')
.all()
return c.json(results)
})
// 단건 조회
app.get('/posts/:id', async (c) => {
const post = await c.env.DB
.prepare('SELECT * FROM posts WHERE id = ?')
.bind(c.req.param('id'))
.first()
if (!post) return c.json({ error: 'Not Found' }, 404)
return c.json(post)
})
// 생성
app.post('/posts', zValidator('json', postSchema), async (c) => {
const { title, content } = c.req.valid('json')
const result = await c.env.DB
.prepare('INSERT INTO posts (title, content) VALUES (?, ?) RETURNING *')
.bind(title, content)
.first()
return c.json(result, 201)
})
app.onError((err, c) => c.json({ error: err.message }, 500))
app.notFound((c) => c.json({ error: 'Not Found' }, 404))
export default app
핵심 정리
import KeyPoints from ’../../components/KeyPoints.astro’
<KeyPoints points={[ “Hono는 ~12 kB 초경량 프레임워크 — Workers의 라우팅·미들웨어 문제를 Express 방식으로 해결”, “c.req.param / c.req.query / c.req.json() 으로 요청 파싱, c.json / c.text / c.html 로 응답”, “app.route(‘/prefix’, subApp) 로 라우터 분리 — 코드베이스 규모 확장에 필수”, “Zod Validator 미들웨어로 입력 검증 + 타입 추론을 동시에 처리”, “타입 파라미터 Hono<{ Bindings }> 로 D1·KV·R2 바인딩 자동 완성 확보” ]} />
import Callout from ’../../components/Callout.astro’