跳到主要内容
中文文档

本页译自英文原文。如有出入,以英文版本为准。

类型化客户端

DaloyJS 提供两种方式,以完全类型安全的方式调用你的 API。根据调用方的情况选择合适的一种。

1. 进程内类型化客户端(零代码生成)

适用于同一 monorepo 中的 TypeScript 调用方(测试、内部工具、Next.js server actions):

ts
import { createInProcessClient } from "@daloyjs/core/client";
import { app } from "./app.js";

const client = createInProcessClient(app);

const r = await client.getBookById({ params: { id: "1" } });
//    ^? { status: 200; body: { id: string; title: string } }
//      | { status: 404; body: ProblemJson }

if (r.status === 200) {
  console.log(r.body.title); // string,完全类型化
}

客户端以 operationId 为键,返回 {status, body, headers} 的可辨识联合,并且一切都从路由定义本身推断而来。不需要构建步骤。

路径参数

路径参数按整个分段替换,因此 :id 永远不会匹配到 :idx 内部,并且每个值都会被百分号编码到它自己的分段中。自 1.3.7 起,当参数缺失、为空、为 . 或 .. 时,生成的方法会在发出任何请求之前抛出 TypeError。URL 解析会解析点分段(即使写成 %2E%2E),从而悄悄改变调用目标,例如把 DELETE /orgs/:org/members/.. 变成 DELETE /orgs/:org。通配符路由(*path,或名为 wildcard 的单独 *)接受以 / 分隔的值;每个分段都会以同样的方式编码和检查。

ts
// app.get("/files/*path", { operationId: "getFile", ... })
await client.getFile({ params: { path: "docs/guide.md" } }); // GET /files/docs/guide.md

await client.getBookById({ params: { id: ".." } });
// TypeError: Invalid path parameter "id" for /books/:id: empty and "."/".." segments are not allowed

2. Hey API SDK(跨语言、跨仓库、构建时生成)

对于 monorepo 之外或使用其他语言的调用方,请用 @hey-api/openapi-ts 生成一个完全类型化的 fetch SDK。

代码生成流水线
  1. 01路由app.route(...)
  2. 02generateOpenAPI@daloyjs/core/openapi
  3. 03openapi.json磁盘上的 OpenAPI 3.1 规范
  4. 04openapi-tsHey API 生成器
  5. 05类型化 SDKsdk.gen.ts · types.gen.ts
  6. 06调用方完全类型化的 fetch 调用
pnpm gen 会运行整条链路:先从路由导出规范,再让 Hey API 把它转换成类型化的 fetch SDK。每当路由变化时重新运行,客户端就会与契约保持同步。
bash
pnpm add -D @hey-api/openapi-ts prettier
ts
// openapi-ts.config.ts
import { defineConfig } from "@hey-api/openapi-ts";

export default defineConfig({
  input: "./generated/openapi.json",
  output: { path: "./generated/client", postProcess: ["prettier"] },
  plugins: ["@hey-api/client-fetch", "@hey-api/typescript", "@hey-api/sdk"],
});
json
// package.json
"scripts": {
  "gen:openapi": "node scripts/dump-openapi.ts",
  "gen:client":  "openapi-ts",
  "gen":         "pnpm gen:openapi && pnpm gen:client"
}
bash
pnpm gen
# 写入:
#   generated/openapi.json
#   generated/client/{client.gen.ts, sdk.gen.ts, types.gen.ts, index.ts}

使用生成的 SDK

ts
import { client } from "./generated/client/client.gen.js";
import { getBookById } from "./generated/client/sdk.gen.js";

client.setConfig({ baseUrl: "https://api.example.com" });

const { data, error } = await getBookById({ path: { id: "1" } });
if (error) console.error(error);
else if (data) console.log(data.title);

我该用哪一种?

使用场景选择
同一仓库中的 TypeScript 调用方(测试、内部工具)createInProcessClient
位于其他仓库的 Web 应用 / React Native 移动端包Hey API SDK
非 TypeScript 调用方(Python、Swift、Kotlin)OpenAPI 文档 + 他们偏好的生成器
面向第三方的公开 SDKHey API SDK,作为独立的包发布

从 ts-rest 迁移过来?

ts-rest 是一个流行的契约优先库,它通过在基于适配器的服务端(Express、Fastify、NestJS、Next.js)和 fetch 客户端(initClient)之间共享一份契约(initContract),提供无需代码生成的端到端 TypeScript 类型。如果你喜欢这种模式,DaloyJS 会让你感到熟悉,只有两点不同。

第一,在 DaloyJS 中,路由定义就是契约,没有需要额外保持同步的独立契约对象。上面展示的进程内 createClient 为同一仓库中的 TypeScript 调用方提供了同样零代码生成、共享类型的体验。

第二,ts-rest 的类型安全仅限 TypeScript,并且要求客户端导入契约。DaloyJS 会从同一组路由生成一等公民的 OpenAPI 3.1 规范和 Hey API SDK,因此无法导入你的类型的调用方(其他仓库、其他语言、公开 SDK)也能覆盖到。在 ts-rest 中,OpenAPI 是一个可选的附加组件(@ts-rest/open-api)。DaloyJS 本身也是服务端和运行时,可在 Node、Bun、Deno、Cloudflare 和 Vercel 之间移植,而不是挂载在另一个框架之上的类型层。

ts-restDaloyJS
契约来源独立的 initContract 对象路由定义本身
零代码生成的类型化客户端有(initClient,仅限 TypeScript)有(createClient,仅限 TypeScript)
跨语言 / 跨仓库客户端OpenAPI 附加组件(@ts-rest/open-api)OpenAPI 3.1 + Hey API SDK,一等支持
服务端基于 Express / Fastify / NestJS / Next.js 的适配器内置,可跨运行时移植
运行时校验Standard Schema(Zod / Valibot / ArkType)Standard Schema(Zod / Valibot / ArkType / TypeBox)
安全默认值自行解决响应头和请求体限制默认开启;CSRF 和限流只需一行代码

需要一个更大的契约来验证生成器的输出?请把大型模拟 REST 演示(英文)作为压力测试场景,而不是使用最小化的教程应用。

最后更新: