Hono
Cloudflare Workers, Deno, Bun, Vercel, AWS Lambda, Node.js를 지원하는 제로 의존성 초고속 엣지 웹 프레임워크.
새 프로젝트 생성
섹션 제목: “새 프로젝트 생성”| 명령어 | 설명 |
|---|---|
npm create hono@latest my-app | npm으로 새 Hono 프로젝트 생성 |
yarn create hono my-app | Yarn으로 생성 |
pnpm create hono my-app | pnpm으로 생성 |
bun create hono my-app | Bun으로 생성 |
deno init --lib hono | Deno로 생성 |
기존 프로젝트에 추가
섹션 제목: “기존 프로젝트에 추가”| 명령어 | 설명 |
|---|---|
npm install hono | 기존 프로젝트에 Hono 설치 |
yarn add hono | Yarn으로 설치 |
pnpm add hono | pnpm으로 설치 |
bun add hono | Bun으로 설치 |
스타터 템플릿
섹션 제목: “스타터 템플릿”# Create project with specific runtime template
npm create hono@latest my-app -- --template cloudflare-workers
npm create hono@latest my-app -- --template bun
npm create hono@latest my-app -- --template deno
npm create hono@latest my-app -- --template nodejs
npm create hono@latest my-app -- --template vercel
npm create hono@latest my-app -- --template aws-lambda
npm create hono@latest my-app -- --template cloudflare-pages
npm create hono@latest my-app -- --template fastly
기본 라우팅
섹션 제목: “기본 라우팅”HTTP 메서드
섹션 제목: “HTTP 메서드”| 명령어 | 설명 |
|---|---|
app.get('/path', handler) | GET 요청 처리 |
app.post('/path', handler) | POST 요청 처리 |
app.put('/path', handler) | PUT 요청 처리 |
app.delete('/path', handler) | DELETE 요청 처리 |
app.patch('/path', handler) | PATCH 요청 처리 |
app.options('/path', handler) | OPTIONS 요청 처리 |
app.all('/path', handler) | 모든 HTTP 메서드 처리 |
app.on('PURGE', '/path', handler) | 커스텀 메서드 처리 |
Hello World
섹션 제목: “Hello World”import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => {
return c.text('Hello Hono!')
})
app.get('/json', (c) => {
return c.json({ message: 'Hello', status: 'ok' })
})
app.get('/html', (c) => {
return c.html('<h1>Hello Hono!</h1>')
})
export default app
라우트 매개변수
섹션 제목: “라우트 매개변수”// Path parameters
app.get('/users/:id', (c) => {
const id = c.req.param('id')
return c.json({ id })
})
// Multiple parameters
app.get('/posts/:postId/comments/:commentId', (c) => {
const { postId, commentId } = c.req.param()
return c.json({ postId, commentId })
})
// Optional parameter
app.get('/api/:version?/users', (c) => {
const version = c.req.param('version') || 'v1'
return c.json({ version })
})
// Wildcard
app.get('/files/*', (c) => {
const path = c.req.path
return c.text(`File path: ${path}`)
})
// Regex-like patterns
app.get('/user/:id{[0-9]+}', (c) => {
const id = c.req.param('id')
return c.json({ id: Number(id) })
})
요청 처리
섹션 제목: “요청 처리”쿼리 매개변수
섹션 제목: “쿼리 매개변수”| 명령어 | 설명 |
|---|---|
c.req.query('key') | 단일 쿼리 매개변수 가져오기 |
c.req.query() | 모든 쿼리 매개변수 가져오기 |
c.req.queries('tags') | 키에 대한 값 배열 가져오기 |
요청 본문
섹션 제목: “요청 본문”| 명령어 | 설명 |
|---|---|
c.req.json() | JSON 본문 파싱 |
c.req.text() | 본문을 텍스트로 가져오기 |
c.req.formData() | 폼 데이터 파싱 |
c.req.blob() | 본문을 Blob으로 가져오기 |
c.req.arrayBuffer() | 본문을 ArrayBuffer로 가져오기 |
c.req.parseBody() | 콘텐츠 타입별 자동 본문 파싱 |
헤더 및 메타데이터
섹션 제목: “헤더 및 메타데이터”| 명령어 | 설명 |
|---|---|
c.req.header('Content-Type') | 요청 헤더 가져오기 |
c.req.header() | 모든 헤더 가져오기 |
c.req.method | HTTP 메서드 가져오기 |
c.req.url | 전체 요청 URL 가져오기 |
c.req.path | 요청 경로 가져오기 |
c.req.raw | 원시 Request 객체 가져오기 |
c.req.valid('json') | 유효성 검사된 데이터 가져오기 |
요청 예시
섹션 제목: “요청 예시”app.post('/users', async (c) => {
const body = await c.req.json()
const page = c.req.query('page') || '1'
const auth = c.req.header('Authorization')
return c.json({
user: body,
page: Number(page),
authenticated: !!auth
}, 201)
})
응답 헬퍼
섹션 제목: “응답 헬퍼”응답 유형
섹션 제목: “응답 유형”| 명령어 | 설명 |
|---|---|
c.text('Hello') | 일반 텍스트 응답 |
c.json({ key: 'value' }) | JSON 응답 |
c.html('<h1>Hi</h1>') | HTML 응답 |
c.redirect('/new-url') | 302 리다이렉트 |
c.redirect('/new-url', 301) | 301 영구 리다이렉트 |
c.notFound() | 404 응답 |
c.body(data) | 원시 본문 응답 |
c.newResponse(body, status, headers) | 완전한 커스텀 응답 |
응답 헤더 및 스트리밍
섹션 제목: “응답 헤더 및 스트리밍”// Set headers
app.get('/api/data', (c) => {
c.header('X-Custom-Header', 'value')
c.header('Cache-Control', 'max-age=3600')
return c.json({ data: 'example' })
})
// Set status code
app.post('/items', async (c) => {
const item = await c.req.json()
return c.json(item, 201)
})
// Stream response
app.get('/stream', (c) => {
return c.streamText(async (stream) => {
await stream.write('Hello ')
await stream.sleep(1000)
await stream.write('World!')
})
})
미들웨어
섹션 제목: “미들웨어”내장 미들웨어
섹션 제목: “내장 미들웨어”| 명령어 | 설명 |
|---|---|
import { cors } from 'hono/cors' | CORS 미들웨어 |
import { logger } from 'hono/logger' | 요청 로깅 |
import { prettyJSON } from 'hono/pretty-json' | JSON 응답 보기 좋게 출력 |
import { basicAuth } from 'hono/basic-auth' | 기본 인증 |
import { bearerAuth } from 'hono/bearer-auth' | Bearer 토큰 인증 |
import { jwt } from 'hono/jwt' | JWT 인증 |
import { compress } from 'hono/compress' | Gzip/Brotli 압축 |
import { etag } from 'hono/etag' | ETag 캐싱 |
import { secureHeaders } from 'hono/secure-headers' | 보안 헤더 |
import { csrf } from 'hono/csrf' | CSRF 보호 |
import { timing } from 'hono/timing' | Server-Timing 헤더 |
import { cache } from 'hono/cache' | 캐시 제어 |
import { bodyLimit } from 'hono/body-limit' | 요청 본문 크기 제한 |
미들웨어 사용법
섹션 제목: “미들웨어 사용법”import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { secureHeaders } from 'hono/secure-headers'
import { bearerAuth } from 'hono/bearer-auth'
const app = new Hono()
// Global middleware
app.use('*', logger())
app.use('*', secureHeaders())
app.use('*', cors({
origin: ['https://example.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400,
}))
// Route-specific middleware
app.use('/api/*', bearerAuth({ token: 'my-secret-token' }))
// Custom middleware
app.use('*', async (c, next) => {
const start = Date.now()
await next()
const duration = Date.now() - start
c.header('X-Response-Time', `${duration}ms`)
})
JWT 인증
섹션 제목: “JWT 인증”import { jwt, sign } from 'hono/jwt'
const app = new Hono()
const SECRET = 'my-secret-key'
app.use('/api/*', jwt({ secret: SECRET }))
app.post('/login', async (c) => {
const { email, password } = await c.req.json()
const token = await sign(
{ email, exp: Math.floor(Date.now() / 1000) + 3600 },
SECRET
)
return c.json({ token })
})
app.get('/api/profile', (c) => {
const payload = c.get('jwtPayload')
return c.json({ email: payload.email })
})
유효성 검사
섹션 제목: “유효성 검사”Zod 유효성 검사기
섹션 제목: “Zod 유효성 검사기”import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const userSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().int().positive().optional(),
})
app.post(
'/users',
zValidator('json', userSchema),
(c) => {
const user = c.req.valid('json')
return c.json(user, 201)
}
)
// Validate query parameters
const querySchema = z.object({
page: z.string().optional().default('1'),
limit: z.string().optional().default('10'),
})
app.get(
'/users',
zValidator('query', querySchema),
(c) => {
const { page, limit } = c.req.valid('query')
return c.json({ page, limit })
}
)
라우팅 그룹
섹션 제목: “라우팅 그룹”const app = new Hono()
// Group routes with basePath
const api = new Hono().basePath('/api')
api.get('/users', (c) => c.json({ users: [] }))
api.post('/users', (c) => c.json({ created: true }))
app.route('/', api)
// Versioned APIs
const v1 = new Hono()
v1.get('/health', (c) => c.json({ status: 'ok' }))
const v2 = new Hono()
v2.get('/health', (c) => c.json({ status: 'ok', version: 2 }))
app.route('/api/v1', v1)
app.route('/api/v2', v2)
오류 처리
섹션 제목: “오류 처리”import { HTTPException } from 'hono/http-exception'
// Custom 404
app.notFound((c) => {
return c.json({ error: 'Not Found' }, 404)
})
// Global error handler
app.onError((err, c) => {
if (err instanceof HTTPException) {
return err.getResponse()
}
console.error(err)
return c.json({ error: 'Internal Server Error' }, 500)
})
// Throw HTTP exceptions
app.get('/admin', (c) => {
if (!c.req.header('Authorization')) {
throw new HTTPException(401, { message: 'Unauthorized' })
}
return c.json({ admin: true })
})
런타임별 설정
섹션 제목: “런타임별 설정”Cloudflare Workers
섹션 제목: “Cloudflare Workers”import { Hono } from 'hono'
type Bindings = {
MY_KV: KVNamespace
MY_DB: D1Database
}
const app = new Hono<{ Bindings: Bindings }>()
app.get('/kv/:key', async (c) => {
const value = await c.env.MY_KV.get(c.req.param('key'))
return c.json({ value })
})
export default app
Node.js
섹션 제목: “Node.js”import { Hono } from 'hono'
import { serve } from '@hono/node-server'
const app = new Hono()
app.get('/', (c) => c.text('Hello Node.js!'))
serve({ fetch: app.fetch, port: 3000 })
Bun
섹션 제목: “Bun”import { Hono } from 'hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Bun!'))
export default { port: 3000, fetch: app.fetch }
Deno
섹션 제목: “Deno”import { Hono } from 'npm:hono'
const app = new Hono()
app.get('/', (c) => c.text('Hello Deno!'))
Deno.serve(app.fetch)
RPC 및 클라이언트
섹션 제목: “RPC 및 클라이언트”타입 안전 RPC 클라이언트
섹션 제목: “타입 안전 RPC 클라이언트”// server.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const app = new Hono()
.get('/api/users', (c) => {
return c.json({ users: [{ id: 1, name: 'Alice' }] })
})
.post(
'/api/users',
zValidator('json', z.object({ name: z.string() })),
(c) => {
const { name } = c.req.valid('json')
return c.json({ id: 2, name }, 201)
}
)
export type AppType = typeof app
export default app
// client.ts — fully typed
import { hc } from 'hono/client'
import type { AppType } from './server'
const client = hc<AppType>('http://localhost:3000')
// Fully typed — autocomplete for routes + responses
const res = await client.api.users.$get()
const data = await res.json()
// data: { users: { id: number, name: string }[] }
테스트
섹션 제목: “테스트”import { testClient } from 'hono/testing'
const app = new Hono()
.get('/hello', (c) => c.json({ message: 'Hello!' }))
// Using testClient (type-safe)
const client = testClient(app)
const res = await client.hello.$get()
const body = await res.json()
// body.message === 'Hello!'
// Using app.request() directly
const res2 = await app.request('/hello')
// res2.status === 200
모범 사례
섹션 제목: “모범 사례”-
환경 바인딩에 TypeScript 제네릭 사용 —
Hono<{ Bindings: MyBindings }>를 정의하여 환경 변수, KV 네임스페이스, D1 데이터베이스에 대한 타입 안전성을 확보하세요. -
모든 입력을 Zod로 유효성 검사 — 요청 본문, 쿼리, 매개변수 유효성 검사에
@hono/zod-validator를 사용하세요. 런타임 안전성과 TypeScript 타입을 한 번에 제공합니다. -
프론트엔드-백엔드 간 RPC 클라이언트 사용 — 서버에서
AppType을 내보내고 클라이언트에서hc<AppType>()을 사용하여 코드 생성 없이 엔드투엔드 타입 안전성을 확보하세요. -
경로 패턴으로 미들웨어 적용 — 모든 것을 전역으로 적용하는 대신
app.use('/api/*', middleware)를 사용하여 특정 라우트에 미들웨어 범위를 지정하세요. -
new Hono().basePath()로 라우트 그룹화 — 관련 엔드포인트를 별도의 Hono 인스턴스로 구성하고app.route()로 마운트하여 깔끔하게 분리하세요. -
전역 오류 처리 —
app.onError()와app.notFound()를 사용하여 모든 오류가 스택 트레이스 대신 구조화된 JSON 응답을 반환하도록 하세요. -
c.executionCtx.waitUntil()사용 — Cloudflare Workers에서 백그라운드 작업(분석, 로깅)에는 응답을 차단하지 않도록waitUntil()을 사용하세요. -
핸들러를 가볍게 유지 — 비즈니스 로직을 별도의 함수/모듈로 추출하세요. 라우트 핸들러는 입력을 파싱하고, 로직을 호출하고, 응답을 반환해야 합니다.
-
testClient로 테스트 — Hono의 내장 테스트 클라이언트는 타입 안전하며 실행 중인 서버가 필요 없어 유닛 테스트를 빠르고 안정적으로 만듭니다. -
사용자가 있는 곳에 배포 — Hono는 모든 주요 엣지 런타임에서 실행됩니다. 글로벌 엣지에는 Cloudflare Workers, 순수 속도에는 Bun, 생태계 호환성에는 Node.js를 선택하세요.