@aipt/api-route
v0.4.4
Published
基于 Hono 与 Zod 的类型安全 API Route 适配。
Readme
API Route
@aipt/[email protected] 让同一份路由定义同时驱动运行时处理器与 OpenAPI 3.1 文档。契约是路由的副产物,不手写,因此不可能与实现漂移。
本包已内含 HTTP 服务能力(依赖 hono 与 @hono/zod-openapi),因此平台不再单独提供「HTTP 服务」代码服务——那只会制造一条能绕过契约的路由方式。
安装与导入
pnpm add @aipt/[email protected]import { ApiRouteFailure, createApiRouter, defineRoute } from "@aipt/api-route";最小用法
import { z } from "zod";
const readAsset = defineRoute({
operationId: "assets.read",
method: "get",
path: "/api/assets/{assetId}",
summary: "读取资产",
params: z.object({ assetId: z.string().min(1) }),
response: z.object({ asset: z.object({ assetId: z.string(), title: z.string() }) }),
errors: [{ status: 404, code: "ASSET_NOT_FOUND", description: "资产不存在" }],
});
const router = createApiRouter({
title: "资产 API", version: "1.0.0",
routes: { readAsset },
publishContractDigest: true,
handlers: {
readAsset: ({ params }) => {
const asset = store.get(params.assetId);
if (!asset) throw new ApiRouteFailure("ASSET_NOT_FOUND", "资产不存在。", 404);
return { asset };
},
},
});
router.fetch(request); // 运行时
router.openApiDocument(); // 同一份定义导出的 OpenAPI 3.1
await router.contractDigest(); // 供客户端比对的契约摘要成功载荷自动包进平台信封 { ok: true, ...载荷 };ApiRouteFailure 包进 { ok: false, error: { code, message } } 并带上声明的状态码。
异步任务与创建
defineRoute({ /* … */ method: "post", successStatus: 202, response: z.object({ task: taskView }) });201 表示已创建,202 表示已受理。GET/DELETE 用它们会在装配期被拒绝。
迁移既有 API
把手写路由迁进来时,响应 schema 写错会让路由从返回数据变成返回 500。用 observe 档位先暴露缺口:
createApiRouter({ /* … */ responseValidation: "observe", onDefect: (defect) => report(defect) });违约会上报 onDefect 但仍然放行,直到缺陷清零再切回默认的 strict。
命令行 dyyto-api
契约随路由每次迭代重算,因此它是动态约束——由命令而非一次性注入的胶水代码承担。四个子命令分成两条流水线:
# 服务端:改完路由 → 重新导出 → 重新生成客户端 → 封存产物
dyyto-api emit --routes ./dist/api-routes.js --out ./openapi.json --export workbenchOpenApiDocument
openapi-ts && dyyto-api seal --dir ./src/generated
# 服务端 CI 门禁:路由改了但契约没重新导出,在这里非零退出
dyyto-api check --routes ./dist/api-routes.js --spec ./openapi.json --export workbenchOpenApiDocument
# 前端 CI 门禁:只持有 openapi.json 的仓库用它,不需要路由模块
dyyto-api digest --spec ./openapi.json --expect sha256:…check 要动态 import 路由模块,只有服务端有;纯前端仓库拿不到路由,用 digest 比对摘要。两端摘要都出自 @aipt/api-contract 的 apiContractDigest,同一份契约必然算出同一个值——把服务端响应头里的 x-dyyto-contract-digest 与本地 spec 的 digest 对上,就知道手上的客户端是不是过期的。
不带 --expect 时 digest 只把摘要打到 stdout,便于流水线捕获后再比对。
错误与边界
API_ROUTE_DEFINITION_INVALID:定义非法(operationId 格式、路径穿越、GET 携带 body、路径参数与paramsschema 不匹配、错误码非大写常量、successStatus用在 GET/DELETE 上)。装配期即失败。API_ROUTE_HANDLER_MISSING:路由没有对应处理器。API_ROUTE_RESPONSE_INVALID:处理器产出违约载荷。畸形载荷绝不发出,返回信封 500,缺陷通过onDefectPort 交给组合根。
其他边界:
- 请求输入在处理器运行前校验;非法输入不会到达业务代码。
- 失败也是契约的一部分:声明过的错误码会进入 OpenAPI 文档的 4xx/5xx 响应。
- 契约摘要挂在响应头,不进响应体——它是跨切面事实,不属于任何一条路由。
- 一个服务由多个路由器组成时,
publishContractDigest传函数让它们公布同一个合并契约的摘要。 - 本包不写日志,不记录请求体或响应体。
退出方式
路由定义是纯数据,schema 由调用方拥有。替换 createApiRouter 后删除依赖即可;已导出的 OpenAPI 文档与生成产物仍可继续使用。
