跳转至主要内容
Learn how to build a server-rendered website with Astro and Deno.

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

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

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

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

🚨️ Deno 2 来了。 🚨️

凭借与 Node/npm 的向后兼容性内置包管理器一体化零配置工具链,以及原生 TypeScriptweb API 支持,编写 JavaScript 从未如此简单。

设置 tRPC

为了开始在 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 服务器

构建 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 对异步可迭代对象的支持

服务器配置为监听端口 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: "https://127.0.0.1:3000",
      }),
      false: unstable_httpBatchStreamLink({
        url: "https://127.0.0.1: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 中修改以下代码片段,以传递 number 而不是 string 作为 description

// ...
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.jsReact 构建实际的前端
  2. 使用 tRPC 中间件将身份验证添加到您的 API
  3. 使用 tRPC 订阅实现实时功能
  4. 为更复杂的数据结构添加输入验证
  5. 与适当的数据库(如 PostgreSQL)集成,或使用 ORM(如 DrizzlePrisma
  6. 将您的应用程序部署到 Deno Deploy通过 Docker 部署到任何公共云

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

🚨️ 想了解更多 Deno 吗? 🚨️

查看我们新的 Learn Deno 教程系列,您将在其中学习

…以及更多内容,以简短、易懂的视频形式呈现。 每周二和周四发布新的教程。