用 tRPC 和 Deno 构建类型安全的 API

客户端与服务端通过共享类型连接的 API 概念图
客户端与服务端共享类型契约。

Deno 是用于编写 JavaScript 和 TypeScript 的一体化、零配置工具链,原生支持 Web 平台 API,非常适合快速构建后端和 API。为了让 API 更易维护,我们可以使用 tRPC。这是一个 TypeScript RPC(远程过程调用)框架,无需声明模式或生成代码,就能构建完全类型安全的 API。

本教程使用 tRPC 和 Deno 构建一个简单的类型安全 API,返回恐龙信息。我们依次配置 tRPC、服务端和客户端,再讨论下一步。教程的全部代码见 GitHub 示例仓库。

Deno 2 已发布。它向后兼容 Node/npm,内置包管理,提供一体化零配置工具链,并原生支持 TypeScript 和 Web API,让 JavaScript 开发更简单。

配置 tRPC

首先安装必需的依赖。借助 Deno 的 npm 兼容性,我们可以使用 npm 版本的 tRPC 包,并用 Zod 进行输入校验:

deno install npm:@trpc/server@next npm:@trpc/client@next npm:zod jsr:@std/path

这条命令安装 tRPC 服务端与客户端包、用于运行时类型校验的 Zod,以及 Deno 标准库的 path 工具。这些包用于在客户端和服务端代码之间构建类型安全的 API 层。

它会在项目根目录创建 deno.json,用于管理 npm 和 JSR 依赖:

{
  "imports": {
    "@std/path": "jsr:@std/path@^1.0.6",
    "@trpc/client": "npm:@trpc/client@^11.0.0-rc.593",
    "@trpc/server": "npm:@trpc/server@^11.0.0-rc.593",
    "zod": "npm:zod@^3.23.8"
  }
}

配置 tRPC 服务端

构建 tRPC 应用的第一步是配置服务端。我们先初始化 tRPC,创建基础路由器与过程构建器,作为定义 API 端点的基础。

创建 server/trpc.ts:

// server/trpc.ts

import { initTRPC } from "@trpc/server";

/**
 * Initialization of tRPC backend
 * Should be done only once per backend!
 */

const t = initTRPC.create();

/**
 * Export reusable router and procedure helpers
 * that can be used throughout the router
 */

export const router = t.router;
export const publicProcedure = t.procedure;

这段代码初始化 tRPC,并导出用于定义 API 端点的路由器与过程构建器。publicProcedure 可以创建不要求身份验证的端点。

接着创建一个简单的数据层来管理恐龙数据。创建 server/db.ts,写入以下内容:

// server/db.ts
import { join } from "@std/path";

type Dino = { name: string; description: string };

const dataPath = join("data", "data.json");

async function readData(): Promise<Dino[]> {
  const data = await Deno.readTextFile(dataPath);
  return JSON.parse(data);
}

async function writeData(dinos: Dino[]): Promise<void> {
  await Deno.writeTextFile(dataPath, JSON.stringify(dinos, null, 2));
}

export const db = {
  dino: {
    findMany: () => readData(),
    findByName: async (name: string) => {
      const dinos = await readData();
      return dinos.find((dino) => dino.name === name);
    },
    create: async (data: { name: string; description: string }) => {
      const dinos = await readData();
      const newDino = { ...data };
      dinos.push(newDino);
      await writeData(dinos);
      return newDino;
    },
  },
};

这会建立一个简单的文件数据库,从 JSON 文件读取恐龙数据并将数据写回文件。生产环境通常需要使用真正的数据库,但这足以满足演示需求。

注意:本教程使用硬编码数据和文件数据库。Deno 也可以连接多种数据库,并使用 Drizzle 或 Prisma 等 ORM。

最后准备实际数据。原文在说明中写作创建 ./data.json,而示例注释与上面的读取路径均为 data/data.json;以下保留原始示例:

// data/data.json
[
  {
    "name": "Aardonyx",
    "description": "An early stage in the evolution of sauropods."
  },
  {
    "name": "Abelisaurus",
    "description": "\"Abel's lizard\" has been reconstructed from a single skull."
  },
  {
    "name": "Abrictosaurus",
    "description": "An early relative of Heterodontosaurus."
  },
  {
    "name": "Abrosaurus",
    "description": "A close Asian relative of Camarasaurus."
  },
  ...
 ]

现在创建主服务端文件,定义 tRPC 路由器与过程。创建 server/index.ts:

// server/index.ts

import { createHTTPServer } from "@trpc/server/adapters/standalone";
import { z } from "zod";
import { db } from "./db.ts";
import { publicProcedure, router } from "./trpc.ts";

const appRouter = router({
  dino: {
    list: publicProcedure.query(async () => {
      const dinos = await db.dino.findMany();
      return dinos;
    }),
    byName: publicProcedure.input(z.string()).query(async (opts) => {
      const { input } = opts;
      const dino = await db.dino.findByName(input);
      return dino;
    }),
    create: publicProcedure
      .input(z.object({ name: z.string(), description: z.string() }))
      .mutation(async (opts) => {
        const { input } = opts;
        const dino = await db.dino.create(input);
        return dino;
      }),
  },
  examples: {
    iterable: publicProcedure.query(async function* () {
      for (let i = 0; i < 3; i++) {
        await new Promise((resolve) => setTimeout(resolve, 500));
        yield i;
      }
    }),
  },
});

// Export type router type signature, this is used by the client.
export type AppRouter = typeof appRouter;

const server = createHTTPServer({
  router: appRouter,
});

server.listen(3000);

这里配置了三个主要的恐龙端点,外加一个异步可迭代对象示例:

  • dino.list:返回所有恐龙。
  • dino.byName:按名称返回指定恐龙。
  • dino.create:创建一条恐龙记录。
  • examples.iterable:演示 tRPC 对异步可迭代对象的支持。

服务端监听 3000 端口,处理所有 tRPC 请求。现在可以启动服务端,但还需要调用这些路由并取得数据的客户端。下面来完成这一步。

配置 tRPC 客户端

服务端准备好后,我们可以创建一个具有完整类型安全保障的 API 客户端。创建 client/index.ts:

// client/index.ts
/**
 * This is the client-side code that uses the inferred types from the server
 */
import {
  createTRPCClient,
  splitLink,
  unstable_httpBatchStreamLink,
  unstable_httpSubscriptionLink,
} from "@trpc/client";
/**
 * We only import the `AppRouter` type from the server - this is not available at runtime
 */
import type { AppRouter } from "../server/index.ts";

// Initialize the tRPC client
const trpc = createTRPCClient<AppRouter>({
  links: [
    splitLink({
      condition: (op) => op.type === "subscription",
      true: unstable_httpSubscriptionLink({
        url: "http://localhost:3000",
      }),
      false: unstable_httpBatchStreamLink({
        url: "http://localhost:3000",
      }),
    }),
  ],
});
const dinos = await trpc.dino.list.query();
console.log("Dinos:", dinos);

const createdDino = await trpc.dino.create.mutate({
  name: "Denosaur",
  description:
    "A dinosaur that lives in the deno ecosystem. Eats Nodes for breakfast.",
});
console.log("Created dino:", createdDino);

const dino = await trpc.dino.byName.query("Denosaur");
console.log("Denosaur:", dino);

const iterable = await trpc.examples.iterable.query();
for await (const i of iterable) {
  console.log("Iterable:", i);
}

客户端代码展示了 tRPC 的几项关键特性:

  1. 从服务端路由器推断类型。客户端通过导入 AppRouter 类型,自动继承服务端的全部类型定义,因此所有 API 调用都获得完整类型支持与编译期类型检查。如果修改服务端过程,TypeScript 会立即指出客户端中不兼容的用法。
  2. 执行查询与变更。示例包含两类 API 调用:查询(list 和 byName)用于无副作用地获取数据;变更(create)用于修改服务端状态。客户端自动了解每个过程的输入和输出类型,在整个请求周期中提供类型安全保障。
  3. 使用异步可迭代对象。examples.iterable 展示了 tRPC 使用异步可迭代对象传输流式数据的能力,尤其适合实时更新或分块处理大数据集。

现在启动服务端观察效果。在 deno.json 配置文件中新增 tasks 属性,包含以下命令:

{
  "tasks": {
    "start": "deno -A server/index.ts",
    "client": "deno -A client/index.ts"
  }
  // Other properties in deno.json remain the same.
}

使用 deno task 列出可用任务:

deno task
Available tasks:
- start
    deno -A server/index.ts
- client
    deno -A client/index.ts

先用 deno task start 启动服务端,待其运行后,再执行 deno task client 运行客户端。原文展示的输出如下:

deno task client
Dinos: [
  {
    name: "Aardonyx",
    description: "An early stage in the evolution of sauropods."
  },
  {
    name: "Abelisaurus",
    description: "Abel's lizard has been reconstructed from a single skull."
  },
  {
    name: "Abrictosaurus",
    description: "An early relative of Heterodontosaurus."
  },
  ...
]
Created dino: {
  name: "Denosaur",
  description: "A dinosaur that lives in the deno ecosystem. Eats Nodes for breakfast."
}
Denosaur: {
  name: "Denosaur",
  description: "A dinosaur that lives in the deno ecosystem. Eats Nodes for breakfast."
}
Iterable: 0
Iterable: 1
Iterable: 2

运行 ./client/index.ts 展示了如何创建 tRPC 客户端,并使用其 JavaScript API 与数据库交互。但是,怎样检查客户端是否从数据库推断出了正确类型?修改 ./client/index.ts 中以下代码,把 description 从 string 改为 number:

// ...
const createdDino = await trpc.dino.create.mutate({
  name: "Denosaur",
  description:
-   "A dinosaur that lives in the deno ecosystem. Eats Nodes for breakfast.",
+   100,
});
console.log("Created dino:", createdDino);
// ...

原文再次运行客户端后得到以下输出:

deno task client
...
error: Uncaught (in promise) TRPCClientError: [
  {
    "code": "invalid_type",
    "expected": "string",
    "received": "number",
    "path": [
      "description"
    ],
    "message": "Expected string, received number"
  }
]
    at Function.from (file:///Users/andyjiang/Library/Caches/deno/npm/registry.npmjs.org/@trpc/client/11.0.0-rc.608/dist/TRPCClientError.mjs:35:20)
    at file:///Users/andyjiang/Library/Caches/deno/npm/registry.npmjs.org/@trpc/client/11.0.0-rc.608/dist/links/httpBatchStreamLink.mjs:118:56
    at eventLoopTick (ext:core/01_core.js:175:7)

tRPC 抛出了 invalid_type 错误,因为它预期接收 string,实际收到的却是 number。

下一步

了解了如何结合 tRPC 与 Deno 后,可以继续:

  1. 使用 Next.js 或 React 构建真正的前端。
  2. 使用 tRPC 中间件为 API 添加身份验证。
  3. 通过 tRPC 订阅实现实时功能。
  4. 为更复杂的数据结构添加输入校验。
  5. 集成 PostgreSQL 等数据库,或采用 Drizzle、Prisma 等 ORM。
  6. 将应用部署到 Deno Deploy,或者通过 Docker 部署到公共云。

祝你使用 Deno 与 tRPC 愉快地进行类型安全开发!

继续学习 Deno:原文还推荐 Learn Deno 教程系列,以短视频介绍一体化工具、浏览器 API,以及使用 Deno 和 Hono 编写 TypeScript API 服务端等主题。新教程于每周二和周四发布。

来源与版权

原文:Build a Typesafe API with tRPC and Deno;作者 Andy Jiang;发布日期 2024 年 11 月 19 日。© Deno Land Inc.,保留所有权利。本中文版本依据转载授权翻译。代码与命令保留原文示例,不表示已在当前版本中实测。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容