OA0 = Omni AI 0
OA0 是一个探索 AI 的论坛
现在注册
已注册用户请  登录
OA0  ›  技能包  ›  trpc-best-practices:针对 tRPC 开发的专家级指导

trpc-best-practices:针对 tRPC 开发的专家级指导

 
  nosql ·  2026-02-14 14:53:41 · 3 次点击  · 0 条评论  

名称: tRPC
描述: tRPC(TypeScript 远程过程调用)专家指南,涵盖路由设置、过程、中间件、上下文、客户端配置及 Next.js 集成。适用于构建类型安全的 API、将 tRPC 与 Next.js 集成,或实现具备完整 TypeScript 类型推断的客户端-服务器通信。


tRPC

提供 tRPC 专业协助——构建端到端类型安全的 API。

概述

tRPC 让你无需模式或代码生成即可构建完全类型安全的 API:
- 从服务器到客户端的完整 TypeScript 类型推断
- 无需代码生成
- 出色的开发体验,支持自动补全和类型安全
- 与 Next.js、React Query 等完美配合

快速开始

安装

# 核心包
npm install @trpc/server@next @trpc/client@next @trpc/react-query@next

# 对等依赖
npm install @tanstack/react-query@latest zod

基础设置(Next.js App Router)

1. 创建 tRPC 路由器

// server/trpc.ts
import { initTRPC } from '@trpc/server'
import { z } from 'zod'

const t = initTRPC.create()

export const router = t.router
export const publicProcedure = t.procedure

2. 定义 API 路由器

// server/routers/_app.ts
import { router, publicProcedure } from '../trpc'
import { z } from 'zod'

export const appRouter = router({
  hello: publicProcedure
    .input(z.object({ name: z.string() }))
    .query(({ input }) => {
      return { greeting: `你好 ${input.name}!` }
    }),

  createUser: publicProcedure
    .input(z.object({
      name: z.string(),
      email: z.string().email(),
    }))
    .mutation(async ({ input }) => {
      const user = await db.user.create({ data: input })
      return user
    }),
})

export type AppRouter = typeof appRouter

3. 创建 API 路由

// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
import { appRouter } from '@/server/routers/_app'

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: () => ({}),
  })

export { handler as GET, handler as POST }

4. 设置客户端 Provider

// app/providers.tsx
'use client'

import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { httpBatchLink } from '@trpc/client'
import { useState } from 'react'
import { trpc } from '@/lib/trpc'

export function Providers({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient())
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: 'http://localhost:3000/api/trpc',
        }),
      ],
    })
  )

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </trpc.Provider>
  )
}

5. 创建 tRPC 客户端

// lib/trpc.ts
import { createTRPCReact } from '@trpc/react-query'
import type { AppRouter } from '@/server/routers/_app'

export const trpc = createTRPCReact<AppRouter>()

6. 在组件中使用

'use client'

import { trpc } from '@/lib/trpc'

export default function Home() {
  const hello = trpc.hello.useQuery({ name: '世界' })
  const createUser = trpc.createUser.useMutation()

  return (
    <div>
      <p>{hello.data?.greeting}</p>
      <button
        onClick={() => createUser.mutate({
          name: '张三',
          email: 'zhangsan@example.com'
        })}
      >
        创建用户
      </button>
    </div>
  )
}

路由器定义

基础路由器

import { router, publicProcedure } from './trpc'
import { z } from 'zod'

export const userRouter = router({
  // Query - 用于获取数据
  getById: publicProcedure
    .input(z.string())
    .query(async ({ input }) => {
      return await db.user.findUnique({ where: { id: input } })
    }),

  // Mutation - 用于创建/更新/删除
  create: publicProcedure
    .input(z.object({
      name: z.string(),
      email: z.string().email(),
    }))
    .mutation(async ({ input }) => {
      return await db.user.create({ data: input })
    }),

  // Subscription - 用于实时更新
  onUpdate: publicProcedure
    .subscription(() => {
      return observable<User>((emit) => {
        // 实现
      })
    }),
})

嵌套路由器

import { router } from './trpc'
import { userRouter } from './routers/user'
import { postRouter } from './routers/post'
import { commentRouter } from './routers/comment'

export const appRouter = router({
  user: userRouter,
  post: postRouter,
  comment: commentRouter,
})

// 客户端使用:
// trpc.user.getById.useQuery('123')
// trpc.post.list.useQuery()
// trpc.comment.create.useMutation()

合并路由器

import { router, publicProcedure } from './trpc'

const userRouter = router({
  list: publicProcedure.query(() => {/* ... */}),
  getById: publicProcedure.input(z.string()).query(() => {/* ... */}),
})

const postRouter = router({
  list: publicProcedure.query(() => {/* ... */}),
  create: publicProcedure.input(z.object({})).mutation(() => {/* ... */}),
})

// 合并到应用路由器
export const appRouter = router({
  user: userRouter,
  post: postRouter,
})

使用 Zod 进行输入验证

基础验证

import { z } from 'zod'

export const userRouter = router({
  create: publicProcedure
    .input(z.object({
      name: z.string().min(2).max(50),
      email: z.string().email(),
      age: z.number().int().positive().optional(),
      role: z.enum(['user', 'admin']),
    }))
    .mutation(async ({ input }) => {
      // input 已完全类型化!
      return await db.user.create({ data: input })
    }),
})

复杂验证

const createPostInput = z.object({
  title: z.string().min(5).max(100),
  content: z.string().min(10),
  published: z.boolean().default(false),
  tags: z.array(z.string()).min(1).max(5),
  metadata: z.object({
    views: z.number().default(0),
    likes: z.number().default(0),
  }).optional(),
})

export const postRouter = router({
  create: publicProcedure
    .input(createPostInput)
    .mutation(async ({ input }) => {
      return await db.post.create({ data: input })
    }),
})

可复用模式

// schemas/user.ts
export const userSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
})

export const createUserSchema = userSchema.omit({ id: true })
export const updateUserSchema = userSchema.partial()

// 在路由器中使用
export const userRouter = router({
  create: publicProcedure
    .input(createUserSchema)
    .mutation(({ input }) => {/* ... */}),

  update: publicProcedure
    .input(z.object({
      id: z.string(),
      data: updateUserSchema,
    }))
    .mutation(({ input }) => {/* ... */}),
})

上下文

创建上下文

// server/context.ts
import { inferAsyncReturnType } from '@trpc/server'
import { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch'

export async function createContext(opts: FetchCreateContextFnOptions) {
  // 从 cookies/headers 获取会话
  const session = await getSession(opts.req)

  return {
    session,
    db,
  }
}

export type Context = inferAsyncReturnType<typeof createContext>

在 tRPC 中使用上下文

// server/trpc.ts
import { initTRPC } from '@trpc/server'
import { Context } from './context'

const t = initTRPC.context<Context>().create()

export const router = t.router
export const publicProcedure = t.procedure

在过程中访问上下文

export const userRouter = router({
  me: publicProcedure.query(({ ctx }) => {
    // ctx.session、ctx.db 可用
    if (!ctx.session) {
      throw new TRPCError({ code: 'UNAUTHORIZED' })
    }

    return ctx.db.user.findUnique({
      where: { id: ctx.session.userId }
    })
  }),
})

中间件

创建中间件

// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server'

const t = initTRPC.context<Context>().create()

// 日志中间件
const loggerMiddleware = t.middleware(async ({ path, type, next }) => {
  const start = Date.now()
  const result = await next()
  const duration = Date.now() - start

  console.log(`${type} ${path} 耗时 ${duration}ms`)

  return result
})

// 认证中间件
const isAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.session) {
    throw new TRPCError({ code: 'UNAUTHORIZED' })
  }

  return next({
    ctx: {
      // 推断会话为非空
      session: ctx.session,
    },
  })
})

// 使用中间件创建过程
export const publicProcedure = t.procedure.use(loggerMiddleware)
export const protectedProcedure = t.procedure.use(loggerMiddleware).use(isAuthed)

使用受保护过程

export const postRouter = router({
  // 公开 - 任何人都可访问
  list: publicProcedure.query(() => {
    return db.post.findMany({ where: { published: true } })
  }),

  // 受保护 - 需要认证
  create: protectedProcedure
    .input(z.object({ title: z.string() }))
    .mutation(({ ctx, input }) => {
      // ctx.session 保证存在
      return db.post.create({
        data: {
          ...input,
          authorId: ctx.session.userId,
        },
      })
    }),
})

基于角色的中间件

const requireRole = (role: string) =>
  t.middleware(({ ctx, next }) => {
    if (!ctx.session || ctx.session.role !== role) {
      throw new TRPCError({ code: 'FORBIDDEN' })
    }
    return next()
  })

export const adminProcedure = protectedProcedure.use(requireRole('admin'))

export const userRouter = router({
  delete: adminProcedure
    .input(z.string())
    .mutation(({ input }) => {
      return db.user.delete({ where: { id: input } })
    }),
})

客户端使用

查询

'use client'

import { trpc } from '@/lib/trpc'

export default function UserList() {
  // 基础查询
  const users = trpc.user.list.useQuery()

  // 带输入的查询
  const user = trpc.user.getById.useQuery('user-123')

  // 禁用查询
  const profile = trpc.user.getProfile.useQuery(
    { id: userId },
    { enabled: !!userId }
  )

  // 带选项的查询
  const posts = trpc.post.list.useQuery(undefined, {
    refetchInterval: 5000,
    staleTime: 1000,
  })

  if (users.isLoading) return <div>加载中...</div>
  if (users.error) return <div>错误:{users.error.message}</div>

  return (
    <ul>
      {users.data?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

变更

'use client'

export default function CreateUser() {
  const utils = trpc.useContext()

  const createUser = trpc.user.create.useMutation({
    onSuccess: () => {
      // 使缓存失效并重新获取
      utils.user.list.invalidate()
    },
    onError: (error) => {
      console.error('创建用户失败:', error)
    },
  })

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const formData = new FormData(e.currentTarget)

    createUser.mutate({
      name: formData.get('name') as string,
      email: formData.get('email') as string,
    })
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" required />
      <input name="email" type="email" required />
      <button type="submit" disabled={createUser.isLoading}>
        {createUser.isLoading ? '创建中...' : '创建用户'}
      </button>
    </form>
  )
}

乐观更新

const updatePost = trpc.post.update.useMutation({
  onMutate: async (newPost) => {
    // 取消正在进行的重新获取
    await utils.post.list.cancel()

    // 快照先前值
    const previousPosts = utils.post.list.getData()

    // 乐观更新
    utils.post.list.setData(undefined, (old) =>
      old?.map(post =>
        post.id === newPost.id ? { ...post, ...newPost } : post
      )
    )

    return { previousPosts }
  },
  onError: (err, newPost, context) => {
    // 出错时回滚
    utils.post.list.setData(undefined, context?.previousPosts)
  },
  onSettled: () => {
    // 成功或出错后重新获取
    utils.post.list.invalidate()
  },
})

无限查询

// 服务器端
export const postRouter = router({
  list: publicProcedure
    .input(z.object({
      cursor: z.string().optional(),
      limit: z.number().min(1).max(100).default(10),
    }))
    .query(async ({ input }) => {
      const posts = await db.post.findMany({
        take: input.limit + 1,
        cursor: input.cursor ? { id: input.cursor } : undefined,
      })

      let nextCursor: string | undefined = undefined
      if (posts.length > input.limit) {
        const nextItem = posts.pop()
        nextCursor = nextItem!.id
      }

      return { posts, nextCursor }
    }),
})

// 客户端
export default function InfinitePosts() {
  const posts = trpc.post.list.useInfiniteQuery(
    { limit: 10 },
    {
      getNextPageParam: (lastPage) => lastPage.nextCursor,
    }
  )

  return (
    <div>
      {posts.data?.pages.map((page, i) => (
        <div key={i}>
          {page.posts.map(post => (
            <div key={post.id}>{post.title}</div>
          ))}
        </div>
      ))}

      <button
        onClick={() => posts.fetchNextPage()}
        disabled={!posts.hasNextPage || posts.isFetchingNextPage}
      >
        {posts.isFetchingNextPage ? '加载中...' : '加载更多'}
      </button>
    </div>
  )
}

错误处理

服务器错误

import { TRPCError } from '@trpc/server'

export const postRouter = router({
  getById: publicProcedure
    .input(z.string())
    .query(async ({ input }) => {
      const post = await db.post.findUnique({ where: { id: input } })

      if (!post) {
        throw new TRPCError({
          code: 'NOT_FOUND',
          message: '文章未找到',
        })
      }

      return post
    }),

  create: protectedProcedure
    .input(z.object({ title: z.string() }))
    .mutation(async ({ ctx, input }) => {
      if (!ctx.session.verified) {
        throw new TRPCError({
          code: 'FORBIDDEN',
          message: '邮箱必须验证',
        })
      }

      try {
        return await db.post.create({ data: input })
      } catch (error) {
        throw new TRPCError({
          code: 'INTERNAL_SERVER_ERROR',
          message: '创建文章失败',
          cause: error,
        })
      }
    }),
})

错误代码

  • BAD_REQUEST - 无效输入
  • UNAUTHORIZED - 未认证
  • FORBIDDEN - 未授权
  • NOT_FOUND - 资源未找到
  • TIMEOUT - 请求超时
  • CONFLICT - 资源冲突
  • PRECONDITION_FAILED - 前置条件检查失败
  • PAYLOAD_TOO_LARGE - 请求过大
  • METHOD_NOT_SUPPORTED - HTTP 方法不支持
  • TOO_MANY_REQUESTS - 请求过多
  • CLIENT_CLOSED_REQUEST - 客户端关闭请求
  • INTERNAL_SERVER_ERROR - 服务器错误

客户端错误处理

```typescript
const createPost = trpc.post.create.useMutation({
onError: (error) => {
if (error.data?.code === 'UNAUTHORIZED') {
router.push('/login')
} else if (error.data?.code === 'FORBIDDEN') {
alert('你没有

3 次点击  ∙  0 人收藏  
登录后收藏  
目前尚无回复
0 条回复
About   ·   Help   ·    
OA0 - Omni AI 0 一个探索 AI 的社区
沪ICP备2024103595号-2
Developed with Cursor