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