deno.com
本页内容

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

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

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

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

设置 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

下一步是什么? 跳转到标题

既然您已经基本了解了如何在 Deno 中使用 tRPC,您可以

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

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

您找到所需的内容了吗?

隐私政策