Appearance
课 1 · Hono 搭建 TypeScript API
本课目标
理解为什么 Agent 项目需要 API 化,用 Hono 搭建类型安全的 TypeScript API 服务。课后你会拿到一个能跑的 apps/api,基础项目的核心能力可以通过 HTTP 调用。
先掌握一条主线:CLI 能做的,API 都能做——只是改成了别人能调用的形式。
基础课里的 Agent 是 CLI 工具:你在终端输入问题,它在终端打印答案。这一课要把它变成一个 HTTP API 服务,让前端页面、其他服务甚至 curl 都能调用它。
为什么需要 API 化
CLI 工具有三个现实限制:
| 限制 | 原因 | API 化后解决 |
|---|---|---|
| 只能一个人用 | 进程绑定终端 | 多个客户端可以同时发请求 |
| 状态不能共享 | 每次启动独立进程 | 会话、缓存可以持久化 |
| 前端无法调用 | 没有网络接口 | 标准 HTTP 接口,任何客户端都能访问 |
进阶课目标不是"更复杂的 Demo",而是把 Agent 能力变成可服务化的系统。API 层是这个系统的入口。
Hono 框架选型
为什么选 Hono 而不是 Express 或 Fastify?
TypeScript API 框架对比
Express老牌,类型支持弱
HonoTypeScript 原生,轻量,边缘兼容
Fastify高性能,配置较重
Hono 的类型推断贯穿路由到处理器,配合 Zod 几乎零冗余。
Hono 的核心优势是类型贯穿:路由参数、请求体、响应都有类型,不需要额外写类型断言。
项目初始化
bash
# 在 advanced/project 目录下
pnpm create hono apps/api
# 选 Node.js 模板安装依赖:
bash
cd apps/api
pnpm add hono @hono/zod-validator zod
pnpm add -D tsx @types/node基础路由结构
typescript
// apps/api/src/index.ts
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { chatRouter } from './routes/chat'
import { docsRouter } from './routes/docs'
const app = new Hono()
// 中间件
app.use('*', logger())
app.use('*', cors())
// 健康检查
app.get('/health', (c) => c.json({ status: 'ok' }))
// 路由挂载
app.route('/api/chat', chatRouter)
app.route('/api/docs', docsRouter)
export default app路由设计原则
API 层和 Agent 层的关键区分:
typescript
// ✅ API 层做的事:接收请求、校验参数、调用业务逻辑、返回响应
app.post('/api/chat', zValidator('json', chatSchema), async (c) => {
const { message, sessionId } = c.req.valid('json')
const answer = await agentService.ask(message, sessionId) // 业务逻辑在这里
return c.json({ answer })
})
// ❌ 不要在路由里直接写 Agent 逻辑
app.post('/api/chat', async (c) => {
const { message } = await c.req.json()
const result = await generateText({ model, messages: [...] }) // 这该放 service 层
return c.json({ result })
})API 层只做三件事:接收 → 校验 → 分发。
参数校验
用 @hono/zod-validator 在路由层统一校验:
typescript
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
const chatSchema = z.object({
message: z.string().min(1).max(2000),
sessionId: z.string().uuid().optional(),
})
chatRouter.post('/', zValidator('json', chatSchema), async (c) => {
const { message, sessionId } = c.req.valid('json') // 已经过校验,类型安全
// ...
})校验失败时 Hono 会自动返回 400,不需要手写 try-catch。
统一错误处理
typescript
// apps/api/src/middleware/error.ts
import type { ErrorHandler } from 'hono'
export const errorHandler: ErrorHandler = (err, c) => {
console.error('[API Error]', err)
if (err.message === 'RATE_LIMIT_EXCEEDED') {
return c.json({ error: '请求过于频繁,请稍后再试' }, 429)
}
return c.json({ error: '内部错误' }, 500)
}在 index.ts 挂载:
typescript
app.onError(errorHandler)本节产物
apps/api/
src/
index.ts # 应用入口、中间件配置
routes/
chat.ts # 问答路由
docs.ts # 文档路由(导入、列表)
services/
agent.ts # Agent 业务逻辑(从基础项目迁移)
middleware/
error.ts # 统一错误处理
package.json课堂实作
- 初始化
apps/api项目 - 实现
GET /health和POST /api/chat - 用 curl 或 HTTPie 验证接口
bash
curl -X POST http://localhost:3000/api/chat \
-H 'Content-Type: application/json' \
-d '{"message": "你好,介绍一下这个知识库"}'并入项目
把基础项目 basic/project/src/agent.ts 里的问答逻辑迁移到 apps/api/src/services/agent.ts,API 路由调用 service,不直接持有 Agent 逻辑。
面试追问
API 层和 Agent 层怎么解耦?
API 层负责 HTTP 协议细节(路由、校验、响应格式),Agent 层负责业务逻辑(工具调用、RAG、上下文管理)。两层通过 service 对象交互,互相不感知对方的实现细节。改 API 框架不影响 Agent,改 Agent 逻辑不影响路由。
为什么要在 API 层做参数校验而不是在 Agent 层?
Agent 层属于核心业务逻辑,应该假设入参是合法的。如果校验放在 Agent 层,每次被调用(HTTP、CLI、测试)都要重复处理格式问题。API 层是系统边界,边界处做校验符合"防御性编程"原则。