tRPC 的特殊之处
tRPC 消除了代码生成的需要。你的 TypeScript 类型会自动从服务器流向客户端——无需 OpenAPI,无需 GraphQL schema,只需 TypeScript。

安装
npm install @trpc/server @trpc/client @trpc/react-query @tanstack/react-query zod
服务器设置
// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { ZodError } from 'zod';
import superjson from 'superjson';
import type { Context } from './context';
const t = initTRPC.context<Context>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError
? error.cause.flatten()
: null,
},
};
},
});
export const router = t.router;
export const publicProcedure = t.procedure;
// 受保护的过程
export const protectedProcedure = t.procedure.use(async ({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({ ctx: { ...ctx, user: ctx.session.user } });
});
上下文
// server/context.ts
import { getServerSession } from 'next-auth';
import { prisma } from '@/lib/prisma';
import type { CreateNextContextOptions } from '@trpc/server/adapters/next';
export async function createContext(opts: CreateNextContextOptions) {
const session = await getServerSession(opts.req, opts.res, authOptions);
return { prisma, session };
}
export type Context = Awaited<ReturnType<typeof createContext>>;
路由和过程
// server/routers/users.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';
import { TRPCError } from '@trpc/server';
export const usersRouter = router({
getById: publicProcedure
.input(z.object({ id: z.string().cuid() }))
.query(async ({ input, ctx }) => {
const user = await ctx.prisma.user.findUnique({
where: { id: input.id },
select: { id: true, name: true, email: true, createdAt: true },
});
if (!user) throw new TRPCError({ code: 'NOT_FOUND' });
return user;
}),
list: publicProcedure
.input(z.object({
page: z.number().min(1).default(1),
perPage: z.number().min(1).max(100).default(20),
search: z.string().optional(),
}))
.query(async ({ input, ctx }) => {
const { page, perPage, search } = input;
const where = search
? { OR: [{ name: { contains: search } }, { email: { contains: search } }] }
: {};
const [users, total] = await Promise.all([
ctx.prisma.user.findMany({
where,
skip: (page - 1) * perPage,
take: perPage,
orderBy: { createdAt: 'desc' },
}),
ctx.prisma.user.count({ where }),
]);
return { users, total, pages: Math.ceil(total / perPage) };
}),
create: protectedProcedure
.input(z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
role: z.enum(['USER', 'ADMIN']).default('USER'),
}))
.mutation(async ({ input, ctx }) => {
const existing = await ctx.prisma.user.findUnique({
where: { email: input.email },
});
if (existing) throw new TRPCError({ code: 'CONFLICT', message: 'Email taken' });
return ctx.prisma.user.create({ data: input });
}),
delete: protectedProcedure
.input(z.object({ id: z.string().cuid() }))
.mutation(async ({ input, ctx }) => {
if (ctx.user.role !== 'ADMIN') {
throw new TRPCError({ code: 'FORBIDDEN' });
}
return ctx.prisma.user.delete({ where: { id: input.id } });
}),
});
App Router
// server/routers/_app.ts
import { router } from '../trpc';
import { usersRouter } from './users';
import { postsRouter } from './posts';
export const appRouter = router({
users: usersRouter,
posts: postsRouter,
});
export type AppRouter = typeof appRouter;
Next.js API 处理器
// app/api/trpc/[trpc]/route.ts (App Router)
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers/_app';
import { createContext } from '@/server/context';
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext,
});
export { handler as GET, handler as POST };
React 客户端使用
// lib/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '@/server/routers/_app';
export const trpc = createTRPCReact<AppRouter>();
// _app.tsx
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import superjson from 'superjson';
const [queryClient] = useState(() => new QueryClient());
const [trpcClient] = useState(() =>
trpc.createClient({
transformer: superjson,
links: [
httpBatchLink({ url: '/api/trpc' }),
],
})
);
// components/UserList.tsx
export function UserList() {
const { data, isLoading } = trpc.users.list.useQuery({ page: 1, perPage: 20 });
const deleteUser = trpc.users.delete.useMutation({
onSuccess: () => utils.users.list.invalidate(),
});
if (isLoading) return <Spinner />;
return (
<ul>
{data?.users.map(user => (
<li key={user.id}>
{user.name}
<button onClick={() => deleteUser.mutate({ id: user.id })}>
Delete
</button>
</li>
))}
</ul>
);
}
订阅(WebSocket)
// 服务器
export const postsRouter = router({
onNewPost: publicProcedure.subscription(async function* () {
for await (const post of pubsub.on('new-post')) {
yield post;
}
}),
});
// 客户端
trpc.posts.onNewPost.useSubscription(undefined, {
onData: (post) => {
console.log('New post:', post);
},
});
为什么选择 tRPC 而非 GraphQL
- tRPC:零 schema,纯 TypeScript,设置更简单
- GraphQL:语言无关,内省,灵活查询
- 选择 tRPC:适用于 TypeScript monorepo 或 Next.js 项目
- 选择 GraphQL:适用于面向多样客户端的公共 API