教程 hono tutorial

Todo API 项目

这个项目把前面内容串起来:模块化路由、Zod 验证、统一错误、请求 ID 和基本 CRUD。

安装依赖

bash
pnpm add hono zod @hono/zod-validator
pnpm add -D tsx typescript

Node.js 运行还需要:

bash
pnpm add @hono/node-server

项目结构

text
src/
├─ index.ts
├─ app.ts
├─ routes/
│  └─ todos.ts
└─ middleware/
   └─ request-id.ts

请求 ID 中间件

ts
// src/middleware/request-id.ts
import type { MiddlewareHandler } from 'hono'

export const requestId: MiddlewareHandler = async (c, next) => {
  const id = c.req.header('X-Request-Id') ?? crypto.randomUUID()
  c.header('X-Request-Id', id)
  await next()
}

Todo 路由

ts
// src/routes/todos.ts
import { Hono } from 'hono'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'

type Todo = {
  id: string
  title: string
  done: boolean
}

const todos = new Map<string, Todo>()

const createTodoSchema = z.object({
  title: z.string().min(1).max(100),
})

const updateTodoSchema = z.object({
  title: z.string().min(1).max(100).optional(),
  done: z.boolean().optional(),
})

export const todoRoutes = new Hono()
  .get('/', (c) => {
    return c.json({ data: Array.from(todos.values()) })
  })
  .post('/', zValidator('json', createTodoSchema), (c) => {
    const input = c.req.valid('json')
    const todo: Todo = {
      id: crypto.randomUUID(),
      title: input.title,
      done: false,
    }

    todos.set(todo.id, todo)

    return c.json({ data: todo }, 201)
  })
  .patch('/:id', zValidator('json', updateTodoSchema), (c) => {
    const id = c.req.param('id')
    const todo = todos.get(id)

    if (!todo) {
      return c.json({ message: 'Todo not found' }, 404)
    }

    const input = c.req.valid('json')
    const nextTodo = { ...todo, ...input }
    todos.set(id, nextTodo)

    return c.json({ data: nextTodo })
  })
  .delete('/:id', (c) => {
    const id = c.req.param('id')

    if (!todos.delete(id)) {
      return c.json({ message: 'Todo not found' }, 404)
    }

    return c.body(null, 204)
  })

组合应用

ts
// src/app.ts
import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { requestId } from './middleware/request-id'
import { todoRoutes } from './routes/todos'

export const app = new Hono()

app.use(logger())
app.use(requestId)

app.get('/health', (c) => c.json({ ok: true }))
app.route('/todos', todoRoutes)

app.notFound((c) => c.json({ message: 'Not Found' }, 404))

app.onError((err, c) => {
  console.error(err)
  return c.json({ message: 'Internal Server Error' }, 500)
})

Node.js 入口

ts
// src/index.ts
import { serve } from '@hono/node-server'
import { app } from './app'

serve({
  fetch: app.fetch,
  port: Number(process.env.PORT ?? '3000'),
})

测试

bash
curl http://localhost:3000/health

curl -X POST http://localhost:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"学习 Hono"}'

这个项目还没有数据库和鉴权,但结构已经能支撑继续扩展。下一步可以把 Map 换成数据库,并为 Todo 增加用户归属。