NestBook / Cloudflare + D1

왜 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

cContext 객체다. 요청 파싱과 응답 생성이 여기에 모여 있다.

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’

**다음 챕터 연결** — REST API가 완성됐다면 이제 프론트엔드가 필요하다. 다음 챕터에서는 **Cloudflare Pages** 를 사용해 정적 사이트를 배포하고, Pages Functions로 백엔드와 프론트엔드를 같은 도메인에서 운영하는 방법을 배운다.