在现代全栈开发中,Turborepo (Monorepo)、Next.js 和 tRPC 的组合构建了一套强大的企业级全栈架构。这种架构的核心优势在于极致的类型安全和代码复用。
本文将结合实际项目代码,介绍如何在 Turborepo 环境下搭建一套高效的 Next.js + tRPC 开发架构。
1. 架构设计:关注点分离
在 Monorepo 中,我们通常将 tRPC 的核心定义抽离为一个独立的 Package,供不同的 App 消费。
1.1 项目结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| . ├── apps │ └── dashboard (Next.js) │ ├── src │ │ ├── app │ │ │ └── api │ │ │ └── trpc │ │ │ └── [trpc] │ │ │ └── route.ts # tRPC API Handler │ │ ├── lib │ │ │ └── trpc │ │ │ ├── client.tsx # tRPC Provider & Client 实例 │ │ │ └── query-client.ts # React Query 配置 │ │ └── components │ │ └── ... # UI 组件调用 │ └── package.json └── packages └── api ├── src │ ├── routers # 业务路由模块 (如 apiKey.ts) │ ├── trpc.ts # tRPC 初始化 & 中间件 │ ├── router.ts # Router & Procedure 定义 │ └── context.ts # Context 定义 ├── index.ts # 导出 AppRouter 类型 └── package.json
|
1.2 核心职责
packages/api: 单一数据源。包含 Router 定义、Zod Schema 和 Procedure 实现。
apps/dashboard: 消费者。导入 packages/api 的类型定义,通过 tRPC Client 调用接口。
2. 核心定义 (packages/api)
这是整个系统的”心脏”。我们在 packages/api 中定义所有的业务逻辑。
2.1 初始化与中间件 (src/trpc.ts)
在 trpc.ts 中,我们初始化 tRPC 实例,并定义通用的中间件(如鉴权)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import { initTRPC, TRPCError } from '@trpc/server' import type { Context } from './context'
const t = initTRPC.context<Context>().create({ errorFormatter({ shape }) { return shape }, })
const isAuthenticated = t.middleware(async ({ ctx, next }) => { if (!ctx.session?.user) { throw new TRPCError({ code: 'UNAUTHORIZED', message: '未登录' }) } return next({ ctx: { session: ctx.session, user: ctx.session.user, }, }) })
export const router = t.router export const publicProcedure = t.procedure export const protectedProcedure = t.procedure.use(isAuthenticated)
|
2.2 定义 Router (src/router.ts)
使用导出的 router 构建 API 路由树。
1 2 3 4 5 6 7 8 9 10
| import { router } from './trpc' import { apiKeyRouter } from './routers/apiKey'
export const appRouter = router({ apiKeyManager: apiKeyRouter, })
export type AppRouter = typeof appRouter
|
3. Next.js 客户端集成 (apps/dashboard)
在 Next.js 应用中,我们需要配置 QueryClient 和 TRPCProvider。
3.1 配置 QueryClient (src/lib/trpc/query-client.ts)
为了支持 SSR 和复杂的数据序列化(如 Date, Map, Set),我们通常配合 superjson 使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query' import superjson from 'superjson'
export function makeQueryClient() { return new QueryClient({ defaultOptions: { queries: { staleTime: 30 * 1000, }, dehydrate: { serializeData: superjson.serialize, shouldDehydrateQuery: (query) => defaultShouldDehydrateQuery(query) || query.state.status === 'pending', }, hydrate: { deserializeData: superjson.deserialize, }, }, }) }
|
3.2 创建 Provider (src/lib/trpc/client.tsx)
这里使用了 Singleton 模式 来确保在客户端只创建一个 QueryClient 实例,避免 React 重渲染时状态丢失。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| 'use client'
import { useState } from 'react' import { createTRPCReact } from '@trpc/react-query' import { httpBatchLink, loggerLink } from '@trpc/client' import { QueryClientProvider } from '@tanstack/react-query' import { makeQueryClient } from './query-client' import type { AppRouter } from '@octopus/api'
export const trpc = createTRPCReact<AppRouter>()
let clientQueryClientSingleton: QueryClient function getQueryClient() { if (typeof window === 'undefined') { return makeQueryClient() } return (clientQueryClientSingleton ??= makeQueryClient()) }
export function TRPCProvider({ children }: { children: React.ReactNode }) { const queryClient = getQueryClient() const [trpcClient] = useState(() => trpc.createClient({ links: [ loggerLink({ enabled: () => true }), httpBatchLink({ url: `${process.env.NEXT_PUBLIC_API_URL}/api/trpc`, }), ], }), )
return ( <trpc.Provider client={trpcClient} queryClient={queryClient}> <QueryClientProvider client={queryClient}>{children}</QueryClientProvider> </trpc.Provider> ) }
|
3.3 API 路由处理 (src/app/api/trpc/[trpc]/route.ts)
Next.js 需要一个 API 路由来处理来自客户端的 tRPC 请求。我们使用 @trpc/server/adapters/fetch 适配器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import { fetchRequestHandler } from '@trpc/server/adapters/fetch' import { appRouter } from '@octopus/api' import { createContextFactory } from '@octopus/api/context' import { auth } from '@/lib/auth'
const createContext = createContextFactory(auth)
const handler = (req: Request) => fetchRequestHandler({ endpoint: '/api/trpc', req, router: appRouter, createContext, })
export { handler as GET, handler as POST }
|
4. 实战案例
在组件中,我们可以直接使用 Hooks 调用接口,全程拥有类型提示。
4.1 获取数据 (useQuery)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 'use client' import { trpc } from '@/lib/trpc/client'
export function KeyList() { const { data, isLoading } = trpc.apiKeyManager.list.useQuery({ page: 1, pageSize: 10, })
if (isLoading) return <div>Loading...</div>
return ( <ul> {data?.data.map((key) => ( <li key={key.id}> {key.name} ({key.platform}) </li> ))} </ul> ) }
|
4.2 修改数据 (useMutation)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 'use client' import { useState } from 'react' import { trpc } from '@/lib/trpc/client'
export function CreateKeyForm() { const [name, setName] = useState('') const utils = trpc.useUtils()
const createKey = trpc.apiKeyManager.create.useMutation({ onSuccess: () => { utils.apiKeyManager.list.invalidate() alert('创建成功!') }, })
const handleSubmit = () => { createKey.mutate({ name, platform: 'openai', key: 'sk-...', baseUrl: 'https://api.openai.com', model: 'gpt-4', }) }
return ( <div> <input value={name} onChange={(e) => setName(e.target.value)} /> <button onClick={handleSubmit} disabled={createKey.isPending}> {createKey.isPending ? 'Creating...' : 'Create'} </button> </div> ) }
|
5. 为什么选择这种架构?
- 跨端类型安全: 修改
packages/api 中的 Zod Schema,Next.js 前端会立即出现红色波浪线报错。重构变得不再可怕。
- 构建性能: Turborepo 的缓存机制确保只有变动的 Package 会被重新构建。
- 高级特性支持: 通过
superjson 支持 Date/Map/Set 等复杂数据类型的无缝传输,无需手动转换。
- 状态管理: 集成 React Query,自动处理缓存、重试、分页和无限加载,极大简化前端逻辑。
通过这种实践,我们不仅实现了代码的物理分离(Monorepo),更实现了逻辑上的紧密耦合(Type Safety),是开发中大型全栈应用的理想选择。