npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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、路径参数与 params schema 不匹配、错误码非大写常量、successStatus 用在 GET/DELETE 上)。装配期即失败。
  • API_ROUTE_HANDLER_MISSING:路由没有对应处理器。
  • API_ROUTE_RESPONSE_INVALID:处理器产出违约载荷。畸形载荷绝不发出,返回信封 500,缺陷通过 onDefect Port 交给组合根。

其他边界:

  • 请求输入在处理器运行前校验;非法输入不会到达业务代码。
  • 失败也是契约的一部分:声明过的错误码会进入 OpenAPI 文档的 4xx/5xx 响应。
  • 契约摘要挂在响应头,不进响应体——它是跨切面事实,不属于任何一条路由。
  • 一个服务由多个路由器组成时,publishContractDigest 传函数让它们公布同一个合并契约的摘要。
  • 本包不写日志,不记录请求体或响应体。

退出方式

路由定义是纯数据,schema 由调用方拥有。替换 createApiRouter 后删除依赖即可;已导出的 OpenAPI 文档与生成产物仍可继续使用。