tRPC 端到端类型安全 API:从路由定义到 Next.js 全栈集成

tRPC 深度实战:procedure/router/middleware 核心概念、Zod 输入校验、React Query 集成、订阅支持、Next.js App Router 适配、与 GraphQL/REST 的混合策略。

一句话总结:tRPC 用 TypeScript 类型消除了 API 契约的重复定义——一个 Router 文件自动生成服务端路由与客户端类型,是全栈 TypeScript 项目最高效的 API 方案。

1. tRPC 核心哲学

tRPC 的设计目标是通过 类型推导零配置 消除传统 API 开发的样板代码。

1.1 核心优势

特性传统 APItRPC
Schema 定义OpenAPI / GraphQL SDL 手写TypeScript 类型自动推导
类型同步手动维护 DTO / codegen变更即同步,零延迟
代码生成Swagger / GraphQL Codegen无需生成,运行时代码复用
包体积SDK 依赖 + 生成代码零运行时 Schema 开销
学习曲线学习 OpenAPI / GraphQL 语法TypeScript 即全部
IDE 支持需插件扩展原生 TypeScript 体验

1.2 对比 GraphQL / REST / gRPC

维度tRPCGraphQLRESTgRPC
类型安全✅ 编译时✅ Schema 层面❌ 文档层面✅ Protobuf
浏览器支持✅ HTTP✅ HTTP✅ HTTP⚠️ Web Proxy
多语言❌ TS only✅ Apollo 全语言✅ 通用✅ 多语言生成
实时✅ Subscription✅ SubscriptionSSE / WS原生双向流
工具生态快速增长成熟丰富成熟云原生成熟

选型法则:全栈 TypeScript + 内部 API → tRPC;多端聚合 + 外部 API → GraphQL;高吞吐微服务间 → gRPC;简单开放 API → REST。


2. 核心概念

2.1 Procedure:API 的基本单元

Procedure 是 tRPC 的"函数",封装了输入校验、授权和逻辑执行。

// router.ts
import { initTRPC } from "@trpc/server";
import { z } from "zod";

const t = initTRPC.create();

// query procedure: 只读操作
const userQuery = t.procedure
  .input(z.object({ id: z.string().uuid() }))
  .query(async ({ input, ctx }) => {
    return ctx.db.user.findUnique({ where: { id: input.id } });
  });

// mutation procedure: 写操作
const createUser = t.procedure
  .input(
    z.object({
      name: z.string().min(1).max(100),
      email: z.string().email(),
    })
  )
  .mutation(async ({ input, ctx }) => {
    return ctx.db.user.create({ data: input });
  });

2.2 Router:Procedure 的命名空间

const appRouter = t.router({
  user: t.router({
    byId: userQuery,
    create: createUser,
    list: t.procedure
      .input(
        z.object({
          cursor: z.string().optional(),
          limit: z.number().min(1).max(100).default(20),
        })
      )
      .query(async ({ input, ctx }) => {
        const users = await ctx.db.user.findMany({
          take: input.limit,
          cursor: input.cursor ? { id: input.cursor } : undefined,
        });
        return {
          users,
          nextCursor: users[users.length - 1]?.id,
        };
      }),
  }),
  post: t.router({
    // ...
  }),
});

export type AppRouter = typeof appRouter;

2.3 Middleware:横切关注点

// 认证中间件
const isAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.user) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }
  return next({
    ctx: {
      ...ctx,
      user: ctx.user, // 类型收窄
    },
  });
});

// 带权限的 procedure
const authedProcedure = t.procedure.use(isAuthed);

// 日志中间件
const logger = t.middleware(async ({ path, type, next }) => {
  const start = Date.now();
  const result = await next();
  console.log(`[${type}] ${path}${Date.now() - start}ms`);
  return result;
});

const loggedProcedure = t.procedure.use(logger);

2.4 Context:请求上下文

// context.ts
import { CreateNextContextOptions } from "@trpc/server/adapters/next";

export async function createContext({ req }: CreateNextContextOptions) {
  const token = req.headers.authorization?.split(" ")[1];
  const user = token ? await verifyToken(token) : null;

  return {
    user,
    db: prisma,
    req,
  };
}

export type Context = Awaited<ReturnType<typeof createContext>>;

2.5 Transformer:序列化增强

import { initTRPC } from "@trpc/server";
import superjson from "superjson";

const t = initTRPC.create({
  transformer: superjson, // 支持 Date, Map, Set, BigInt
});

tRPC 默认使用 JSON.stringify,无法序列化 Date 等类型。superjson 插件完美解决这个问题。


3. 完整 CRUD 实战

3.1 服务端

// server/routers/post.ts
import { router, publicProcedure, protectedProcedure } from "../trpc";
import { z } from "zod";

export const postRouter = router({
  list: publicProcedure
    .input(
      z.object({
        search: z.string().optional(),
        tag: z.string().optional(),
        limit: z.number().min(1).max(50).default(10),
        page: z.number().min(1).default(1),
      })
    )
    .query(async ({ input, ctx }) => {
      const where = {
        ...(input.search && {
          title: { contains: input.search },
        }),
        ...(input.tag && {
          tags: { has: input.tag },
        }),
      };

      const [posts, total] = await Promise.all([
        ctx.db.post.findMany({
          where,
          take: input.limit,
          skip: (input.page - 1) * input.limit,
          orderBy: { createdAt: "desc" },
          include: { author: true },
        }),
        ctx.db.post.count({ where }),
      ]);

      return {
        posts,
        pagination: {
          page: input.page,
          limit: input.limit,
          total,
          totalPages: Math.ceil(total / input.limit),
        },
      };
    }),

  byId: publicProcedure
    .input(z.object({ id: z.string().uuid() }))
    .query(async ({ input, ctx }) => {
      const post = await ctx.db.post.findUnique({
        where: { id: input.id },
        include: { author: true, comments: true },
      });
      if (!post) {
        throw new TRPCError({
          code: "NOT_FOUND",
          message: "Post not found",
        });
      }
      return post;
    }),

  create: protectedProcedure
    .input(
      z.object({
        title: z.string().min(1).max(200),
        content: z.string().min(1).max(50000),
        tags: z.array(z.string().max(50)).max(10),
        published: z.boolean().default(false),
      })
    )
    .mutation(async ({ input, ctx }) => {
      return ctx.db.post.create({
        data: {
          ...input,
          authorId: ctx.user.id,
        },
      });
    }),

  update: protectedProcedure
    .input(
      z.object({
        id: z.string().uuid(),
        title: z.string().min(1).max(200).optional(),
        content: z.string().min(1).max(50000).optional(),
        published: z.boolean().optional(),
      })
    )
    .mutation(async ({ input, ctx }) => {
      const { id, ...data } = input;
      return ctx.db.post.update({
        where: { id, authorId: ctx.user.id },
        data,
      });
    }),
});

3.2 客户端 React Hooks

// hooks/usePosts.ts
import { trpc } from "@/utils/trpc";

export function usePosts(search?: string) {
  return trpc.post.list.useQuery({
    search,
    limit: 10,
    page: 1,
  });
}

// components/PostList.tsx
export function PostList() {
  const { data, isLoading, fetchNextPage } = trpc.post.list.useInfiniteQuery(
    { limit: 10 },
    {
      getNextPageParam: (lastPage) => lastPage.pagination.nextCursor,
    }
  );

  if (isLoading) return <Skeleton />;

  return (
    <div>
      {data?.pages.map((page) =>
        page.posts.map((post) => <PostCard key={post.id} post={post} />)
      )}
      <button onClick={() => fetchNextPage()}>加载更多</button>
    </div>
  );
}

// mutation
export function CreatePostForm() {
  const utils = trpc.useContext();
  const mutation = trpc.post.create.useMutation({
    onSuccess: () => {
      utils.post.list.invalidate(); // 自动刷新列表
    },
  });

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        mutation.mutate({
          title: "新文章",
          content: "内容...",
          tags: ["trpc"],
        });
      }}
    >
      {/* ... */}
    </form>
  );
}

4. React Query 集成

tRPC 底层使用 TanStack Query(原 React Query),所有 Query 的缓存、重试、预取、乐观更新能力完整保留。

4.1 核心 Hooks

Hook用途对应 Query
useQuery读取数据Query
useInfiniteQuery无限滚动/分页Query
useMutation修改数据Mutation
useSubscription实时推送Subscription
useContext获取 QueryClient

4.2 乐观更新

const mutation = trpc.post.like.useMutation({
  onMutate: async (postId) => {
    await utils.post.byId.cancel({ id: postId });

    const previousPost = utils.post.byId.getData({ id: postId });

    utils.post.byId.setData({ id: postId }, (old) =>
      old ? { ...old, likeCount: old.likeCount + 1 } : old
    );

    return { previousPost };
  },
  onError: (err, postId, context) => {
    utils.post.byId.setData({ id: postId }, context?.previousPost);
  },
  onSettled: (postId) => {
    utils.post.byId.invalidate({ id: postId });
  },
});

4.3 预取

// 鼠标悬停时预取
function PostLink({ id }: { id: string }) {
  const utils = trpc.useContext();

  return (
    <Link
      href={`/posts/${id}`}
      onMouseEnter={() => utils.post.byId.prefetch({ id })}
    >
      查看文章
    </Link>
  );
}

5. Next.js 集成

5.1 Pages Router

// pages/api/trpc/[trpc].ts
import { createNextApiHandler } from "@trpc/server/adapters/next";
import { appRouter } from "@/server/routers/_app";
import { createContext } from "@/server/context";

export default createNextApiHandler({
  router: appRouter,
  createContext,
  onError: ({ error, path }) => {
    console.error(`[tRPC Error] ${path}: ${error.message}`);
  },
});

5.2 App Router (RSC)

// app/posts/page.tsx (Server Component)
import { appRouter } from "@/server/routers/_app";
import { createContext } from "@/server/context";

export default async function PostsPage() {
  const caller = appRouter.createCaller(await createContext());
  const posts = await caller.post.list({ limit: 10 });

  return (
    <div>
      {posts.map((post) => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  );
}

5.3 Client Component 混合

// app/posts/PostList.tsx
"use client";

import { trpc } from "@/utils/trpc";

export function PostList() {
  const { data, isLoading } = trpc.post.list.useQuery({ limit: 10 });
  // ...
}

6. 订阅(Subscription)

// server/routers/notification.ts
import { observable } from "@trpc/server/observable";

export const notificationRouter = router({
  onNewNotification: protectedProcedure
    .subscription(({ ctx }) => {
      return observable<string>((emit) => {
        const handler = (data: string) => emit.next(data);

        // 订阅 Redis PubSub
        redisSub.subscribe(`user:${ctx.user.id}:notifications`);
        redisSub.on("message", handler);

        return () => {
          redisSub.unsubscribe(`user:${ctx.user.id}:notifications`);
          redisSub.off("message", handler);
        };
      });
    }),
});

客户端

function NotificationBadge() {
  const { data } = trpc.notification.onNewNotification.useSubscription(
    undefined,
    {
      onData: (data) => {
        toast(data);
      },
    }
  );

  return <Badge count={unreadCount} />;
}

7. 与 GraphQL / REST 的混合策略

7.1 三层 API 架构

┌─────────────────────────────────────┐
│   多端客户端(Web / iOS / Android)   │
└──────────────┬──────────────────────┘
               │ GraphQL (Apollo Client)
               ▼
┌─────────────────────────────────────┐
│   GraphQL Gateway (Apollo Router)    │
│   聚合层 / BFF / 权限控制             │
└──────────────┬──────────────────────┘
               │ tRPC / gRPC
               ▼
┌─────────────────────────────────────┐
│   TypeScript 微服务(tRPC)            │
│   内部 API,端到端类型安全              │
└─────────────────────────────────────┘

7.2 何时用 tRPC,何时用 GraphQL

场景推荐
全栈 TypeScript 内部 APItRPC
移动端 / 第三方接入GraphQL
已有 GraphQL 生态(Federation)GraphQL
需要 Swagger / OpenAPI 文档REST / GraphQL
AI / LLM 工具调用GraphQL(标准化 schema)

8. 生产最佳实践

8.1 错误处理

// 统一错误格式化
import { TRPCError } from "@trpc/server";
import { initTRPC } from "@trpc/server";

const t = initTRPC.create({
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError:
          error.cause instanceof ZodError
            ? error.cause.flatten()
            : null,
      },
    };
  },
});

8.2 Auth 集成

// NextAuth / Clerk 集成
export const protectedProcedure = t.procedure
  .use(async ({ ctx, next }) => {
    const user = await getAuth(ctx.req);
    if (!user) throw new TRPCError({ code: "UNAUTHORIZED" });
    return next({ ctx: { ...ctx, user } });
  });

8.3 部署

平台适配器
Vercel Edgefetch adapter
Vercel Nodenext adapter
AWS Lambdaaws-lambda adapter
Expressexpress middleware
Fastifyfastify plugin

9. 一句话总结

  • 核心理念:TypeScript 类型即 API 契约,零重复、零代码生成
  • Router + Procedure:命名空间组织 API,input/output 天然类型安全
  • Middleware:认证、日志、缓存复用,类似 Express 中间件
  • React Query:底层缓存、重试、乐观更新、预取能力完整继承
  • Next.js:Pages Router / App Router / RSC 全适配
  • 混合策略:tRPC(内部 TS 全栈)+ GraphQL(多端聚合)互补

FAQ

Q1:tRPC 是否绑定 Next.js?

A:不绑定。tRPC 支持多种框架:Next.js、React(Vite / CRA)、Svelte、Vue、React Native、Express、Fastify 等。Next.js 集成最完善,但非必需。

Q2:非 TypeScript 客户端(如 Swift / Kotlin)怎么调用 tRPC?

A:tRPC 仅天生支持 TypeScript。如需多语言接入,可在 tRPC 服务端增加 OpenAPI 生成(zod-to-openapi),或语言桥接层(如 Kotlin 通过 HTTP POST 调用并手动维护类型)。这种情况下 GraphQL 或 REST 更合适。

Q3:tRPC 的性能与 REST/GraphQL 相比如何?

A:tRPC 使用 HTTP JSON,序列化开销与 REST 相当。无 Schema 解析和查询规划开销,所以多数场景比 GraphQL 更快。与 gRPC(二进制 Protobuf)相比,吞吐量和延迟稍差,但开发效率更高。

Q4:如何实现 tRPC 的限流?

A:在 middleware 中集成限流库(如 @upstash/ratelimit):

const rateLimit = t.middleware(async ({ path, ctx, next }) => {
  const result = await ratelimit.limit(ctx.user?.id ?? ctx.req.ip);
  if (!result.success) {
    throw new TRPCError({ code: "TOO_MANY_REQUESTS" });
  }
  return next();
});

Q5:tRPC 的订阅在服务端如何扩展?

A:tRPC Subscription 基于 WebSocket,多节点部署时需要共享事件源。建议:① Redis PubSub 做跨节点广播;② 使用 SSE 方案替代 WebSocket(部分 adapter 支持);③ 结合 Ably / Pusher 等托管实时服务。


相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「API 工程」更多文章

  1. GraphQL vs REST vs gRPC vs tRPC:API 范式深度对比与选型
  2. GraphQL Schema 演进与版本控制:零破化变更策略
  3. API 网关实战:Kong、Envoy 与 Traefik 选型与部署