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 的几项关键特性:
- 从服务端路由器推断类型。客户端通过导入
AppRouter类型,自动继承服务端的全部类型定义,因此所有 API 调用都获得完整类型支持与编译期类型检查。如果修改服务端过程,TypeScript 会立即指出客户端中不兼容的用法。 - 执行查询与变更。示例包含两类 API 调用:查询(
list和byName)用于无副作用地获取数据;变更(create)用于修改服务端状态。客户端自动了解每个过程的输入和输出类型,在整个请求周期中提供类型安全保障。 - 使用异步可迭代对象。
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 后,可以继续:
- 使用 Next.js 或 React 构建真正的前端。
- 使用 tRPC 中间件为 API 添加身份验证。
- 通过 tRPC 订阅实现实时功能。
- 为更复杂的数据结构添加输入校验。
- 集成 PostgreSQL 等数据库,或采用 Drizzle、Prisma 等 ORM。
- 将应用部署到 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.,保留所有权利。本中文版本依据转载授权翻译。代码与命令保留原文示例,不表示已在当前版本中实测。











暂无评论内容