Node.js HTTP 与路由

要点

路由定位处理者,中间件提供公共前后处理,校验把不可信输入转成业务可用值。TypeScript 类型不会验证请求体。

路由:方法 + 路径 + 处理器

基本路由与路径参数

import { Hono } from "hono";

const app = new Hono();

// 路径参数 c.req.param();查询字符串 c.req.query()
app.get("/posts/:postId/comments", (c) => {
  const postId = c.req.param("postId");
  const limit = Number(c.req.query("limit") ?? 10);
  return c.json({ postId, limit });
});

// 多种 HTTP 方法
app.post("/posts", (c) => c.json({ id: "new" }, 201));

// 带正则约束的参数:只有纯数字才命中
app.delete("/posts/:id{[0-9]+}", (c) => c.json({ deleted: c.req.param("id") }));

// 未匹配路由的兜底:app.notFound(不是通配符路由)
app.notFound((c) => c.json({ error: "路由不存在" }, 404));

子应用拆分:真实项目的组织方式

Hono 没有独立的 Router 构造器——每个路由模块就是一个 Hono 实例:

// src/routes/users.ts(目录含 index.ts 挂载总路由,users/posts 为子应用)
import { Hono } from "hono";
export const usersApp = new Hono();
usersApp.get("/", listUsers);
usersApp.get("/:id", getUser);
usersApp.post("/", createUser);
// src/routes/index.ts —— 总路由聚合,server 装配时 app.route("/api", apiApp)
import { Hono } from "hono";
import { usersApp } from "./users.js";
import { postsApp } from "./posts.js";

export const apiApp = new Hono();
apiApp.route("/users", usersApp);
apiApp.route("/posts", postsApp);

中间件:洋葱模型流水线

执行模型

中间件是 (c, next) => Promise 函数,按注册顺序”进入”,逆序”返回”——await next() 两侧代码分别对应请求进站与响应出站:

// 1) await next()      → 进入下一个中间件,之后还能修改响应
// 2) 抛出错误          → 跳过后续逻辑,直达 app.onError
// 3) 不调用 next 也不返回响应 → 请求悬挂(响应须在该中间件内结束)
app.use(async (c, next) => {
  const start = performance.now();
  await next();
  const ms = performance.now() - start;
  c.res.headers.set("X-Response-Time", `${ms.toFixed(1)}ms`); // next() 之后改响应
});

洋葱模型相比线性管道的优势:进站与出站逻辑写在同一个函数里,计时、清理、事务包裹都不需要”前中间件 + 后中间件”两个文件。

常见中间件形态

// 内置:开发期日志
import { logger } from "hono/logger";
app.use(logger());

// 自定义认证中间件
import type { MiddlewareHandler } from "hono";

// Context 变量需要显式类型:中间件与 app 标注同一 Env 泛型(全局增强写法见 TypeScript 模式)
type Env = { Variables: { user: User } };

const requireAuth: MiddlewareHandler<Env> = async (c, next) => {
  const token = c.req.header("Authorization")?.replace("Bearer ", "");
  if (!token) return c.json({ error: "未认证" }, 401);
  c.set("user", verifyToken(token));   // 写入 Context 变量
  await next();
};

// 作用域挂载:只保护某个路由组(app 需以同一 Env 创建:new Hono<Env>())
app.use("/api/admin/*", requireAuth);
// 或路由级
app.get("/me", requireAuth, (c) => c.json({ user: c.get("user") }));

顺序即语义:认证必须在读取 c.get("user") 的处理器之前;bodyLimit 必须在解析请求体之前。

请求校验:Zod

不校验的接口等于裸奔——类型欺骗、注入、500 全都会来。Zod 是 Node 生态当前事实标准的校验库:schema 即类型来源。

pnpm add zod

定义 Schema 与推导类型

import { z } from "zod";

// 定义 schema
const CreateUserSchema = z.object({
  name: z.string().min(1).max(50),
  email: z.email(),                        // Zod 4 顶层 API(旧版 z.string().email())
  age: z.number().int().min(0).max(150).optional(),
  role: z.enum(["admin", "member"]).default("member"),
});

// 从 schema 推导 TypeScript 类型——单一事实来源
type CreateUserInput = z.infer<typeof CreateUserSchema>;
// { name: string; email: string; age?: number; role: "admin" | "member" }

zValidator:校验中间件的官方姿势

社区官方推荐的 @hono/zod-validator 把校验挂进路由,校验后的数据通过 c.req.valid() 取回(类型自动收窄):

pnpm add -D @hono/zod-validator
import { zValidator } from "@hono/zod-validator";

app.post("/users", zValidator("json", CreateUserSchema), (c) => {
  const data = c.req.valid("json"); // CreateUserInput 类型,无需再断言
  return c.json(createUser(data), 201);
});

校验失败时默认返回 400;要定制错误响应,给 zValidator 传第三个回调参数(模式详解见 TypeScript 模式)。

不想引入中间件也可以手写——在处理器里 safeParse 后把 ZodError 抛给 app.onError 统一输出(见 错误处理)。

完整 API 速查见 Zod 与生态库指南。

可复现验收:解析 JSON 与接受业务输入是两步

下面程序不依赖 Hono 或 Zod;它把处理器必须面对的两个边界固定下来。语法正确的 {} 可以被 JSON 解析,但仍缺少必填 title;空白 title 也不是可创建任务。框架 schema 应产生同类区分,只是把分支与错误格式统一封装。

function parseCreateTask(text) {
  let body;
  try {
    body = JSON.parse(text);
  } catch {
    return { status: 400, code: "invalid_json" };
  }
  if (typeof body.title !== "string" || body.title.trim() === "") {
    return { status: 422, code: "invalid_title" };
  }
  return { status: 201, title: body.title.trim() };
}

for (const input of ['{"title":"  ship docs  "}', '{"title":"   "}', '{}', '{']) {
  console.log(JSON.stringify(parseCreateTask(input)));
}

预期输出:

{"status":201,"title":"ship docs"}
{"status":422,"code":"invalid_title"}
{"status":422,"code":"invalid_title"}
{"status":400,"code":"invalid_json"}

实际 API 还应限制请求体大小、拒绝未知字段或按产品需求保留它们,并把错误响应中的 code 固定为客户端可依赖的契约;不要把 JSON.parse 的原始异常文本返回给外部调用者。

最佳实践

路由按资源或业务边界组织,读请求、校验、调用服务和写响应各有明确责任,不用文件超过某行数就强制拆分。客户端的 JSON、路径和查询参数都属于待验证输入,转换成功还要检查范围及业务合法性。

中间件适合提取和验证身份,但“当前用户能否修改这条记录”的授权通常还涉及业务数据。统一字段错误响应让前端能定位输入问题;用非法参数、无身份、跨用户和成功请求分别验收。

常见问题

Q1: 请求体一直是空对象/抛错?

A: 确认请求头 Content-Type: application/json 与 c.req.json() 配套;parseBody() 只用于 multipart 表单,两者不能混用。

Q2: c.req.query("page") 类型为什么是 string | undefined?

A: 查询串天然是字符串。用 z.coerce.number() 或 Number(...) 显式转换,并给默认值兜底。

练习与实践

练习一:任务清单 API

任务要求: 1. 用子应用实现 /api/tasks 的 GET/POST/PATCH/DELETE 2. 每个接口配 Zod schema(含分页参数校验)+ zValidator 3. 加一个记录耗时的中间件(进站记时间,出站写 X-Response-Time 头)

练习二:中间件依赖链

挑战任务: – 实现 requireRole("admin") 中间件工厂,依赖认证中间件先执行 – 故意颠倒注册顺序,观察并解释报错行为

提示: 角色信息从认证中间件写入的 c.get("user") 读取。


相关文档

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇