Chapter 03

Hono 框架:极速 HTTP 服务

零依赖、多运行时兼容的超轻量框架——Cloudflare Workers、Bun、Node.js 通吃

为什么是 Hono

JavaScript 后端框架众多——Express(老牌、生态最大)、Fastify(高性能)、Koa(Express 进化版)……Hono 的定位是:在任何 JavaScript 运行时上都能运行的超轻量框架

与主流框架性能对比

框架req/s(Bun)包大小运行时支持
Hono~120,00013KBBun/Node/CF/Deno/边缘
Fastify~85,000~65KBNode.js
Express~42,000~220KBNode.js
Koa~60,000~26KBNode.js

快速开始

# 安装 Hono
bun add hono
# 或 npm install hono
// src/index.ts — 最简示例(同时支持 Bun 和 Node.js)
import { Hono } from 'hono';

const app = new Hono();

app.get('/', (c) => c.text('Hello Hono!'));

app.get('/hello/:name', (c) => {
  const name = c.req.param('name');
  return c.json({ message: `Hello, ${name}!` });
});

// Bun 运行
export default app;
// 然后 bun run src/index.ts

// Node.js 运行(需要适配器)
import { serve } from '@hono/node-server';
serve({ fetch: app.fetch, port: 3000 });

路由系统

基础路由

const app = new Hono();

// HTTP 方法
app.get('/users', getAllUsers);
app.post('/users', createUser);
app.put('/users/:id', updateUser);
app.delete('/users/:id', deleteUser);
app.patch('/users/:id', patchUser);

// 路径参数
app.get('/users/:id', (c) => {
  const id = c.req.param('id');         // 单个参数
  const { id: uid } = c.req.param();  // 所有参数
  return c.json({ id });
});

// 查询参数
app.get('/search', (c) => {
  const q = c.req.query('q') ?? '';
  const page = Number(c.req.query('page') ?? '1');
  return c.json({ q, page });
});

// 通配符
app.get('/static/*', serveStatic);

// 正则路由
app.get('/post/:id{[0-9]+}', (c) => { ... });

路由分组(Router)

import { Hono } from 'hono';

// 创建子路由
const api = new Hono().basePath('/api');
const v1 = new Hono();

const userRouter = new Hono();
userRouter.get('/', getAllUsers);
userRouter.get('/:id', getUser);
userRouter.post('/', createUser);

const postRouter = new Hono();
postRouter.get('/', getAllPosts);
postRouter.post('/', createPost);

// 组合路由
v1.route('/users', userRouter);
v1.route('/posts', postRouter);
api.route('/v1', v1);

const app = new Hono();
app.route('', api);

// 最终路由:GET /api/v1/users、POST /api/v1/posts 等

export default app;

中间件系统

Hono 的中间件与 Express/Koa 类似,但使用 Web 标准的 Request/Response API,并支持类型推断:

import { Hono } from 'hono';
import { logger } from 'hono/logger';
import { cors } from 'hono/cors';
import { bearerAuth } from 'hono/bearer-auth';
import { compress } from 'hono/compress';
import { timing } from 'hono/timing';

const app = new Hono();

// 内置中间件 — 全局应用
app.use(logger());   // 请求日志
app.use(timing());   // Server-Timing 头
app.use(compress()); // gzip 压缩

// CORS 配置
app.use(cors({
  origin: ['https://myapp.com', 'http://localhost:5173'],
  allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
}));

// 自定义中间件(带类型扩展)
type Env = {
  Variables: {
    userId: string;
    user: { id: string; name: string };
  };
};

const authMiddleware = createMiddleware<Env>(async (c, next) => {
  const token = c.req.header('Authorization')?.replace('Bearer ', '');
  if (!token) {
    return c.json({ error: 'Unauthorized' }, 401);
  }
  // 验证 token,设置用户信息到上下文
  c.set('userId', 'user-123');
  await next(); // 调用下一个中间件/处理器
});

// 对特定路由应用中间件
app.use('/api/*', authMiddleware);
app.get('/api/profile', (c) => {
  const userId = c.get('userId'); // 类型安全
  return c.json({ userId });
});

请求与响应 API

// Context (c) 对象的常用 API
app.post('/demo', async (c) => {
  // 读取请求
  const body = await c.req.json();         // JSON body
  const text = await c.req.text();         // 文本 body
  const form = await c.req.formData();     // 表单数据
  const blob = await c.req.blob();         // 二进制

  const header = c.req.header('Content-Type');
  const method = c.req.method;
  const url = c.req.url;
  const path = c.req.path;

  // 发送响应
  return c.json({ ok: true });            // JSON
  return c.text('Hello');                 // 纯文本
  return c.html('<h1>Hi</h1>');           // HTML
  return c.redirect('/new-url', 301);     // 重定向
  return c.body(buffer, { status: 200,   // 自定义
    headers: { 'Content-Type': 'image/png' }
  });

  // 流式响应(SSE/大文件)
  return c.streamText(async (stream) => {
    await stream.writeln('data: hello\n');
    await stream.sleep(1000);
    await stream.writeln('data: world\n');
  });
});

请求验证(Zod Validator)

Hono 提供了与 Zod 深度集成的验证中间件,让输入验证和类型推断同步完成:

bun add zod @hono/zod-validator
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';

// 定义 Schema
const createUserSchema = z.object({
  name: z.string().min(2).max(50),
  email: z.string().email(),
  age: z.number().int().min(0).max(150).optional(),
  role: z.enum(['user', 'admin']).default('user'),
});

const userIdSchema = z.object({
  id: z.string().uuid(),
});

const querySchema = z.object({
  page: z.coerce.number().default(1),
  limit: z.coerce.number().max(100).default(20),
  search: z.string().optional(),
});

app.post(
  '/users',
  zValidator('json', createUserSchema),   // 验证请求体
  async (c) => {
    const data = c.req.valid('json'); // 已验证,类型安全
    // data.name: string
    // data.email: string
    // data.role: 'user' | 'admin'
    return c.json({ id: 'new-id', ...data }, 201);
  }
);

app.get(
  '/users',
  zValidator('query', querySchema),       // 验证查询参数
  async (c) => {
    const { page, limit, search } = c.req.valid('query');
    return c.json({ page, limit, search });
  }
);

// 自定义验证错误响应
zValidator('json', createUserSchema, (result, c) => {
  if (!result.success) {
    return c.json({
      error: 'Validation failed',
      details: result.error.flatten(),
    }, 400);
  }
});

错误处理

import { HTTPException } from 'hono/http-exception';

// 抛出 HTTP 异常
app.get('/protected', (c) => {
  throw new HTTPException(401, { message: '需要登录' });
});

// 全局错误处理器
app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return err.getResponse();
  }
  console.error(err);
  return c.json({ error: 'Internal Server Error' }, 500);
});

// 404 处理
app.notFound((c) => {
  return c.json({ error: `路径 ${c.req.path} 不存在` }, 404);
});

OpenAPI / Swagger 集成

使用 @hono/zod-openapi 可以从 Zod Schema 自动生成 OpenAPI 文档:

bun add @hono/zod-openapi @hono/swagger-ui
import { OpenAPIHono, createRoute, z } from '@hono/zod-openapi';
import { swaggerUI } from '@hono/swagger-ui';

const app = new OpenAPIHono();

const UserSchema = z.object({
  id: z.string().openapi({ example: 'user-123' }),
  name: z.string().openapi({ example: '张三' }),
}).openapi('User');

const getUserRoute = createRoute({
  method: 'get',
  path: '/users/{id}',
  summary: '获取用户信息',
  tags: ['Users'],
  request: {
    params: z.object({ id: z.string() }),
  },
  responses: {
    200: {
      content: { 'application/json': { schema: UserSchema } },
      description: '成功返回用户',
    },
    404: { description: '用户不存在' },
  },
});

app.openapi(getUserRoute, async (c) => {
  const { id } = c.req.valid('param');
  return c.json({ id, name: '张三' });
});

// 生成 OpenAPI JSON
app.doc('/openapi.json', {
  openapi: '3.0.0',
  info: { title: 'My API', version: '1.0.0' },
});

// Swagger UI 界面
app.get('/docs', swaggerUI({ url: '/openapi.json' }));

export default app;

Hono RPC:Hono 还提供了 hono/client,可以从服务端路由定义直接生成类型安全的客户端调用,类似轻量版 tRPC。对于不需要完整 tRPC 的项目是很好的选择。

完整 CRUD API 示例

// src/routes/posts.ts — 博客文章 CRUD
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';

type Post = { id: string; title: string; content: string; createdAt: Date };
const posts = new Map<string, Post>();

const createPostSchema = z.object({
  title: z.string().min(1).max(200),
  content: z.string().min(1),
});

export const postsRouter = new Hono()
  .get('/', (c) => {
    return c.json([...posts.values()]);
  })
  .get('/:id', (c) => {
    const post = posts.get(c.req.param('id'));
    if (!post) return c.json({ error: 'Not found' }, 404);
    return c.json(post);
  })
  .post('/', zValidator('json', createPostSchema), async (c) => {
    const data = c.req.valid('json');
    const post: Post = { id: crypto.randomUUID(), ...data, createdAt: new Date() };
    posts.set(post.id, post);
    return c.json(post, 201);
  })
  .delete('/:id', (c) => {
    const deleted = posts.delete(c.req.param('id'));
    if (!deleted) return c.json({ error: 'Not found' }, 404);
    return c.body(null, 204);
  });