ProductReadyProductReady

tRPC API 指南

使用 tRPC v11 构建类型安全的 API - 完整指南与示例

tRPC API 指南

ProductReady 使用 tRPC v11 构建类型安全的 API。这意味着您可以获得完整的自动补全类型安全运行时验证,从后端到前端 - 无需代码生成!

为什么选择 tRPC? 只需编写一次 API,前端自动获得 TypeScript 类型。无需手动类型定义,无需同步问题。


什么是 tRPC?

tRPC 让您构建的 API 具有以下特性:

  • 类型安全 - 前端完全知道后端返回什么
  • 处处自动补全 - 编辑器帮您编写代码
  • 已验证 - 使用 Zod 在运行时检查输入/输出
  • 快速构建 - 无需 REST 样板代码,无需 GraphQL 模式

示例:5 行代码创建 API 端点,在前端使用时具有完整类型!


项目结构

src/
├── server/
│   ├── trpc.ts              # tRPC 设置和中间件
│   ├── context/             # 请求上下文(用户、数据库)
│   └── routers/             # API 端点
│       ├── index.ts         # 主路由(组合所有路由)
│       ├── agentTasks.ts         # 任务 CRUD
│       └── posts.ts         # 帖子 CRUD
├── app/
│   └── api/
│       └── trpc/
│           └── [trpc]/route.ts  # Next.js API 路由处理器
└── lib/
    └── trpc/
        └── client.tsx       # 前端 tRPC 客户端

基本概念

1. 过程(Procedures)

过程就是您的 API 端点。有两种类型:

  • Query(查询) - 获取数据(GET)
  • Mutation(变更) - 修改数据(POST/PUT/DELETE)
// Query 示例
export const tasksRouter = createTRPCRouter({
  // GET /api/trpc/agentTasks.list
  list: publicProcedure.query(async ({ ctx }) => {
    return ctx.db.select().from(agentTasks);
  }),
  
  // POST /api/trpc/agentTasks.create
  create: protectedProcedure
    .input(z.object({ title: z.string() }))
    .mutation(async ({ ctx, input }) => {
      return ctx.db.insert(agentTasks).values(input);
    }),
});

2. 上下文(Context)

上下文是所有过程都可以访问的数据(用户会话、数据库等):

// src/server/context/index.ts
export async function createContext({ req, res }: CreateNextContextOptions) {
  const session = await getSession(req, res);
  
  return {
    session,      // 当前用户
    db,           // 数据库客户端
  };
}

3. 中间件(Middleware)

中间件在过程之前运行(身份验证检查、日志记录等):

// src/server/trpc.ts
const isAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.session?.user) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return next({
    ctx: {
      session: ctx.session,  // TypeScript 现在知道 session 存在!
    },
  });
});

// 在过程中使用
export const protectedProcedure = t.procedure.use(isAuthed);

创建第一个 API 端点

让我们从头开始构建一个"项目"功能!

创建数据库模式

创建 src/db/schema/projects.ts

import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { users } from './users';

export const projects = pgTable('projects', {
  id: uuid('id').defaultRandom().primaryKey(),
  name: text('name').notNull(),
  description: text('description'),
  ownerId: text('owner_id').references(() => users.id).notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull(),
});

创建 tRPC 路由

创建 src/server/routers/projects.ts

import { z } from 'zod';
import { createTRPCRouter, protectedProcedure } from '../trpc';
import { projects } from '~/db/schema/projects';
import { eq } from 'drizzle-orm';

export const projectsRouter = createTRPCRouter({
  // 列出用户的项目
  list: protectedProcedure.query(async ({ ctx }) => {
    return ctx.db
      .select()
      .from(projects)
      .where(eq(projects.ownerId, ctx.session.user.id));
  }),

  // 获取单个项目
  get: protectedProcedure
    .input(z.object({ id: z.string().uuid() }))
    .query(async ({ ctx, input }) => {
      const project = await ctx.db
        .select()
        .from(projects)
        .where(eq(projects.id, input.id))
        .limit(1);
      
      if (!project[0]) {
        throw new TRPCError({ code: 'NOT_FOUND' });
      }
      
      return project[0];
    }),

  // 创建项目
  create: protectedProcedure
    .input(z.object({
      name: z.string().min(1).max(100),
      description: z.string().max(500).optional(),
    }))
    .mutation(async ({ ctx, input }) => {
      const [project] = await ctx.db
        .insert(projects)
        .values({
          ...input,
          ownerId: ctx.session.user.id,
        })
        .returning();
      
      return project;
    }),
});

注册路由

添加到 src/server/routers/index.ts

import { projectsRouter } from './projects';

export const appRouter = createTRPCRouter({
  agentTasks: tasksRouter,
  posts: postsRouter,
  projects: projectsRouter,  // ← 添加这行
});

在前端使用

现在在 React 组件中使用,具有完整的类型安全:

'use client';

import { trpc } from '~/lib/trpc/client';

export function ProjectsList() {
  // 完整的自动补全!✨
  const { data: projects, isLoading } = trpc.projects.list.useQuery();
  
  const createMutation = trpc.projects.create.useMutation({
    onSuccess: () => {
      // 重新获取项目列表
      utils.projects.list.invalidate();
    },
  });

  async function handleCreate() {
    await createMutation.mutateAsync({
      name: '我的新项目',
      description: '使用 ProductReady 构建',
    });
  }

  if (isLoading) return <div>加载中...</div>;

  return (
    <div>
      <button onClick={handleCreate}>创建项目</button>
      
      {projects?.map((project) => (
        <div key={project.id}>
          <h3>{project.name}</h3>
          <p>{project.description}</p>
        </div>
      ))}
    </div>
  );
}

完成! 您现在拥有一个完全类型安全的 CRUD API,无需代码生成。前端自动知道所有类型!


常见模式

分页

export const projectsRouter = createTRPCRouter({
  list: protectedProcedure
    .input(z.object({
      cursor: z.number().default(0),
      limit: z.number().min(1).max(100).default(20),
    }))
    .query(async ({ ctx, input }) => {
      const items = await ctx.db
        .select()
        .from(projects)
        .limit(input.limit + 1)
        .offset(input.cursor);
      
      let nextCursor: number | undefined;
      if (items.length > input.limit) {
        items.pop();
        nextCursor = input.cursor + input.limit;
      }
      
      return {
        items,
        nextCursor,
      };
    }),
});

前端使用无限滚动:

const { data, fetchNextPage, hasNextPage } = 
  trpc.projects.list.useInfiniteQuery(
    { limit: 20 },
    { getNextPageParam: (lastPage) => lastPage.nextCursor }
  );

最佳实践

✅ 推荐做法

  • 使用 Zod 进行输入验证 - 运行时安全
  • 保持路由小巧 - 每个领域一个路由(用户、项目等)
  • 使用 protectedProcedure 进行身份验证 - 永远不要信任前端
  • 返回类型化对象 - 避免 any 类型
  • 明确处理错误 - 使用 TRPCError 代码

下一步


资源

On this page