콘텐츠로 이동

Hono

Cloudflare Workers, Deno, Bun, Vercel, AWS Lambda, Node.js를 지원하는 제로 의존성 초고속 엣지 웹 프레임워크.

명령어설명
npm create hono@latest my-appnpm으로 새 Hono 프로젝트 생성
yarn create hono my-appYarn으로 생성
pnpm create hono my-apppnpm으로 생성
bun create hono my-appBun으로 생성
deno init --lib honoDeno로 생성
명령어설명
npm install hono기존 프로젝트에 Hono 설치
yarn add honoYarn으로 설치
pnpm add honopnpm으로 설치
bun add honoBun으로 설치
# 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
명령어설명
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)커스텀 메서드 처리
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.methodHTTP 메서드 가져오기
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`)
})
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 })
})
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 })
})
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
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 })
import { Hono } from 'hono'

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

export default { port: 3000, fetch: app.fetch }
import { Hono } from 'npm:hono'

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

Deno.serve(app.fetch)
// 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
  1. 환경 바인딩에 TypeScript 제네릭 사용Hono<{ Bindings: MyBindings }>를 정의하여 환경 변수, KV 네임스페이스, D1 데이터베이스에 대한 타입 안전성을 확보하세요.

  2. 모든 입력을 Zod로 유효성 검사 — 요청 본문, 쿼리, 매개변수 유효성 검사에 @hono/zod-validator를 사용하세요. 런타임 안전성과 TypeScript 타입을 한 번에 제공합니다.

  3. 프론트엔드-백엔드 간 RPC 클라이언트 사용 — 서버에서 AppType을 내보내고 클라이언트에서 hc<AppType>()을 사용하여 코드 생성 없이 엔드투엔드 타입 안전성을 확보하세요.

  4. 경로 패턴으로 미들웨어 적용 — 모든 것을 전역으로 적용하는 대신 app.use('/api/*', middleware)를 사용하여 특정 라우트에 미들웨어 범위를 지정하세요.

  5. new Hono().basePath()로 라우트 그룹화 — 관련 엔드포인트를 별도의 Hono 인스턴스로 구성하고 app.route()로 마운트하여 깔끔하게 분리하세요.

  6. 전역 오류 처리app.onError()app.notFound()를 사용하여 모든 오류가 스택 트레이스 대신 구조화된 JSON 응답을 반환하도록 하세요.

  7. c.executionCtx.waitUntil() 사용 — Cloudflare Workers에서 백그라운드 작업(분석, 로깅)에는 응답을 차단하지 않도록 waitUntil()을 사용하세요.

  8. 핸들러를 가볍게 유지 — 비즈니스 로직을 별도의 함수/모듈로 추출하세요. 라우트 핸들러는 입력을 파싱하고, 로직을 호출하고, 응답을 반환해야 합니다.

  9. testClient로 테스트 — Hono의 내장 테스트 클라이언트는 타입 안전하며 실행 중인 서버가 필요 없어 유닛 테스트를 빠르고 안정적으로 만듭니다.

  10. 사용자가 있는 곳에 배포 — Hono는 모든 주요 엣지 런타임에서 실행됩니다. 글로벌 엣지에는 Cloudflare Workers, 순수 속도에는 Bun, 생태계 호환성에는 Node.js를 선택하세요.