tRPC 结合了 REST 和 GraphQL 的概念。如果您对两者都不熟悉,请查看关键的概念。
安装
tRPC 分布在几个包中,因此您可以只安装您需要的包。请确保在代码库的正确部分安装您想要的包。对于这个快速入门指南,我们将保持简单,只使用普通客户端。对于框架指南,请查看 与 React 的使用 和 与 Next.js 的使用。
要求
- tRPC 需要 TypeScript >= 4.7.0
- 我们强烈建议您在
tsconfig.json
中使用"strict": true
,因为我们不正式支持非严格模式。
首先,安装 @trpc/server
和 @trpc/client
包:
- npm
- yarn
- pnpm
- bun
npm install @trpc/server@next @trpc/client@next
yarn add @trpc/server@next @trpc/client@next
pnpm add @trpc/server@next @trpc/client@next
bun add @trpc/server@next @trpc/client@next
定义后端路由器
让我们逐步构建一个使用 tRPC 的类型安全 API。首先,这个 API 将包含三个端点,具有以下 TypeScript 签名:
type User = { id: string; name: string; };
userList: () => User[];
userById: (id: string) => User;
userCreate: (data: { name: string }) => User;
1. 创建路由器实例
首先,让我们初始化 tRPC 后端。通常的做法是在单独的文件中进行此操作,并导出可重用的辅助函数,而不是整个 tRPC 对象。
// server/trpc.ts
import { initTRPC } from '@trpc/server';
/**
* tRPC 后端的初始化
* 每个后端只应执行一次!
*/
const t = initTRPC.create();
/**
* 导出可重用的路由器和过程助手
* 可以在路由器中使用
*/
export const router = t.router;
export const publicProcedure = t.procedure;
接下来,我们将初始化我们的主要路由器实例,通常称为 appRouter
,在其中我们将稍后添加过程。最后,我们需要导出路由器的类型,稍后在客户端使用。
// server/index.ts
import { router } from './trpc';
const appRouter = router({
// ...
});
// 导出路由器类型的签名,
// 而不是路由器本身。
export type AppRouter = typeof appRouter;
2. 添加查询过程
使用 publicProcedure.query()
向路由器添加查询过程。
以下创建了一个名为 userList
的查询过程,它从我们的数据库返回用户列表:
// server/index.ts
import { db } from './db';
import { publicProcedure, router } from './trpc';
const appRouter = router({
userList: publicProcedure
.query(async () => {
// 从数据源检索用户,这是一个假想的数据库
const users = await db.user.findMany();
return users;
}),
});
3. 使用输入解析器验证过程输入
要实现 userById
过程,我们需要接受来自客户端的输入。tRPC 允许您定义输入解析器来验证和解析输入。您可以定义自己的输入解析器或使用您选择的验证库,如 zod、yup 或 superstruct。
您在 publicProcedure.input()
上定义输入解析器,然后在解析器函数中可以像下面所示访问:
// server/index.ts
const appRouter = router({
// ...
userById: publicProcedure
.input((val: unknown) => {
// 如果值是字符串类型,返回它。
// 它现在将被推断为字符串。
if (typeof val === 'string') return val;
// 哦哦,看起来那个输入不是字符串。
// 我们将抛出一个错误,而不是运行过程。
throw new Error(`Invalid input: ${typeof val}`);
})
.query(async (opts) => {
const { input } = opts;
const user = await db.user.findById(input);
return user;
}),
});
输入解析器可以是任何 ZodType
,例如 z.string()
或 z.object()
。
// server.ts
import { z } from 'zod';
const appRouter = router({
// ...
userById: publicProcedure
.input(z.string())
.query(async (opts) => {
const { input } = opts;
const user = await db.user.findById(input);
return user;
}),
});
输入解析器可以是任何 YupSchema
,例如 yup.string()
或 yup.object()
。
// server.ts
import * as yup from 'yup';
const appRouter = router({
// ...
userById: publicProcedure
.input(yup.string().required())
.query(async (opts) => {
const { input } = opts;
const user = await db.user.findById(input);
return user;
}),
});
使用 Valibot,您只需要用 TypeSchema 包装您的模式。
// server.ts
import { wrap } from '@typeschema/valibot';
import { string } from 'valibot';
const appRouter = router({
// ...
userById: publicProcedure
.input(wrap(string()))
.query(async (opts) => {
const { input } = opts;
const user = await db.user.findById(input);
return user;
}),
});
信息
在本文档的剩余部分中,我们将使用 zod
作为我们的验证库。
4. 添加变异过程
类似于 GraphQL,tRPC 在查询和变异过程之间做出区分。
过程在服务器上的工作原理在查询和变异之间没有太大变化。方法名称不同,客户端使用此过程的方式也不同,但其他一切都相同!
让我们通过将 userCreate
作为我们路由器对象上的一个新属性来添加一个变异:
// server.ts
const appRouter = router({
// ...
userCreate: publicProcedure
.input(z.object({ name: z.string() }))
.mutation(async (opts) => {
const { input } = opts;
const user = await db.user.create(input);
return user;
}),
});
服务 API
现在我们已经定义了我们的路由器,我们可以提供它。tRPC 有许多适配器,因此您可以选择任何后端框架。为了保持简单,我们将使用 standalone
适配器。
// server/index.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
const appRouter = router({
// ...
});
const server = createHTTPServer({
router: appRouter,
});
server.listen(3000);
查看完整的后端代码
// server/db.ts
type User = { id: string; name: string };
// 假想的数据库
const users: User[] = [];
export const db = {
user: {
findMany: async () => users,
findById: async (id: string) => users.find((user) => user.id === id),
create: async (data: { name: string }) => {
const user = { id: String(users.length + 1), ...data };
users.push(user);
return user;
},
},
};
// server/trpc.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const router = t.router;
export const publicProcedure = t.procedure;
// server/index.ts
import { createHTTPServer } from '@trpc/server/adapters/standalone';
import { z } from 'zod';
import { db } from './db';
import { publicProcedure, router } from './trpc';
const appRouter = router({
userList: publicProcedure
```ts
.query(async () => {
const users = await db.user.findMany();
return users;
}),
userById: publicProcedure
.input(z.string())
.query(async (opts) => {
const { input } = opts;
const user = await db.user.findById(input);
return user;
}),
userCreate: publicProcedure
.input(z.object({ name: z.string() }))
.mutation(async (opts) => {
const { input } = opts;
const user = await db.user.create(input);
return user;
}),
});
export type AppRouter = typeof appRouter;
const server = createHTTPServer({
router: appRouter,
});
server.listen(3000);
在客户端使用您的新后端
现在让我们转到客户端代码,并拥抱端到端类型安全的威力。当我们为客户端使用导入 AppRouter
类型时,我们已经实现了我们系统的完全类型安全,而没有向客户端泄露任何实现细节。
1. 设置 tRPC 客户端
// client/index.ts
import { createTRPCClient, httpBatchLink } from '@trpc/client';
import type { AppRouter } from './server';
// 👆 **仅类型** 导入
// 将 AppRouter 作为泛型传递。👇 这允许 `trpc` 对象知道
// 服务器上有哪些可用的过程以及它们的输入/输出类型。
const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:3000',
}),
],
});
在 tRPC 中的链接类似于 GraphQL 中的链接,它们让我们控制数据流 在 被发送到服务器之前。在上面的例子中,我们使用了 httpBatchLink,它会自动将多个调用批量到一个 HTTP 请求中。有关链接的更深入使用,请查看 链接文档。
2. 查询和变异
您现在可以在 trpc
对象上访问您的 API 过程。试试看!
// Inferred types
const user = await trpc.userById.query('1');
const user: { name: string; id: string; } | undefined
const createdUser = await trpc.userCreate.mutate({ name: 'sachinraja' });
const createdUser: { name: string; id: string; }
完整的自动完成
您可以打开您的 Intellisense 来探索前端上的 API。您将发现所有的程序路由都在等待您,以及调用它们的方法。
// Full autocompletion on your routes
trpc.u;
* userById
* userCreate
* userList
请注意,由于您提供的文本包含了一些用于网页布局的CSS媒体查询代码,这些代码在这里没有实际用途,因此我已将它们省略。如果您需要在特定的编程或技术环境中应用这些格式,请确保遵循相应的指南和最佳实践。