deno.com
本页内容

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

Deno 是一个一体化、零配置的工具链,用于编写 JavaScript 和 TypeScript,并原生支持 Web 平台 API,使其成为快速构建后端和 API 的理想选择。为了使我们的 API 更易于维护,我们可以使用 tRPC,一个 TypeScript RPC(远程过程调用)框架,它允许您在不进行模式声明或代码生成的情况下构建完全类型安全的 API。

在本教程中,我们将使用 tRPC 和 Deno 构建一个简单的类型安全 API,该 API 返回恐龙信息

您可以在这个 GitHub 仓库中找到本教程的所有代码。

设置 tRPC Jump to heading

要在 Deno 中开始使用 tRPC,我们需要安装所需的依赖项。得益于 Deno 的 npm 兼容性,我们可以将 tRPC 包的 npm 版本与 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 服务器 Jump to heading

构建 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 文件中的恐龙数据。在生产环境中,您通常会使用适当的数据库,但这对于我们的演示来说效果很好。

⚠️ 在本教程中,我们硬编码数据并使用基于文件的数据库。但是,您可以连接到各种数据库并使用 ORM,例如 DrizzlePrisma

最后,我们需要提供实际数据。让我们创建一个包含一些示例恐龙数据的 ./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 对异步可迭代对象(async iterables)的支持

服务器配置为监听端口 3000,并将处理所有 tRPC 请求。

虽然您现在可以运行服务器,但您将无法访问任何路由并使其返回数据。让我们来解决这个问题!

设置 tRPC 客户端 Jump to heading

服务器准备就绪后,我们可以创建一个客户端,以完全类型安全的方式使用我们的 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 调用:用于获取数据且无副作用的查询(listbyName),以及用于修改服务器端状态的操作(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 与数据库交互。但是我们如何检查 tRPC 客户端是否从数据库推断出了正确的类型呢?让我们修改 ./client/index.ts 中的以下代码片段,将 description 传入 number 类型而不是 string 类型

// ...
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 类型。

下一步是什么? Jump to heading

现在您已经对如何在 Deno 中使用 tRPC 有了基本的了解,您可以

  1. 使用 Next.jsReact 构建实际的前端
  2. 使用 tRPC 中间件为您的 API 添加身份验证
  3. 使用 tRPC 订阅实现实时功能
  4. 为更复杂的数据结构添加输入验证
  5. PostgreSQL 等合适的数据库集成,或使用 DrizzlePrisma 等 ORM
  6. 将您的应用程序部署到 Deno Deploy通过 Docker 部署到任何公共云

🦕 使用 Deno 和 tRPC 愉快地进行类型安全编程!

您找到所需内容了吗?

隐私政策