tRPCMiddleTechnical
Как объединить несколько tRPC-роутеров в один корневой роутер?
Несколько роутеров объединяются через t.router({ users: userRouter, posts: postRouter }). В tRPC v11 также доступен t.mergeRouters() для плоского слияния без вложенности.
Объединение роутеров в tRPC
В реальных приложениях API разбивается на доменные роутеры (users, posts, auth и т.д.), которые потом объединяются в один корневой appRouter. tRPC поддерживает два способа объединения.
Способ 1: вложенные роутеры (рекомендуется)
// server/routers/user.ts
import { router, protectedProcedure } from '../trpc';
import { z } from 'zod';
export const userRouter = router({
getMe: protectedProcedure.query(({ ctx }) => ctx.user),
update: protectedProcedure
.input(z.object({ name: z.string().min(1) }))
.mutation(({ input, ctx }) => {
return db.user.update({
where: { id: ctx.user.id },
data: { name: input.name },
});
}),
});
// server/routers/post.ts
import { router, publicProcedure, protectedProcedure } from '../trpc';
import { z } from 'zod';
export const postRouter = router({
list: publicProcedure.query(() => db.post.findMany()),
create: protectedProcedure
.input(z.object({ title: z.string(), body: z.string() }))
.mutation(({ input, ctx }) => {
return db.post.create({
data: { ...input, authorId: ctx.user.id },
});
}),
});
// server/routers/_app.ts
import { router } from '../trpc';
import { userRouter } from './user';
import { postRouter } from './post';
import { authRouter } from './auth';
export const appRouter = router({
user: userRouter, // → trpc.user.getMe()
post: postRouter, // → trpc.post.list()
auth: authRouter, // → trpc.auth.login()
});
export type AppRouter = typeof appRouter;
Способ 2: t.mergeRouters() — плоское слияние (v11)
Если нужны процедуры на верхнем уровне без вложенности:
// server/routers/_app.ts
import { t } from '../trpc';
import { userRouter } from './user';
import { postRouter } from './post';
// Процедуры объединяются в одно пространство имён
export const appRouter = t.mergeRouters(userRouter, postRouter);
// Результат: trpc.getMe(), trpc.list() — без префиксов
export type AppRouter = typeof appRouter;
Экспорт типа AppRouter
Критически важно экспортировать только тип, а не сам роутер, на клиент:
// Правильно — только тип, без runtime-импорта серверного кода
export type { AppRouter } from '@/server/routers/_app';
// Неправильно — утащит весь серверный код в клиентский бандл
import { appRouter } from '@/server/routers/_app';
Подключение к HTTP-хендлеру
// app/api/trpc/[trpc]/route.ts (Next.js App Router)
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/routers/_app';
import { createContext } from '@/server/context';
const handler = (req: Request) =>
fetchRequestHandler({
endpoint: '/api/trpc',
req,
router: appRouter,
createContext,
});
export { handler as GET, handler as POST };
Подводные камни
- Конфликты имён в mergeRouters: если два роутера имеют одноимённые процедуры, один перезапишет другой без предупреждения TypeScript.
- Circular imports: если роутеры импортируют друг друга, возникнет циклическая зависимость — разбивайте на слои (db → services → routers).
- AppRouter тип нельзя импортировать на клиент как значение: только
import type, иначе серверный код попадёт в браузер. - Глубокая вложенность усложняет рефакторинг: больше двух уровней вложенности (
trpc.admin.user.ban()) сложно поддерживать. - Нельзя смешивать роутеры из разных initTRPC: типы будут несовместимы.
- mergeRouters в v11 заменяет устаревший router.merge(): не используйте старый API.
- Горячая перезагрузка в Next.js dev: при изменении роутера может потребоваться перезапуск из-за кэша модулей.
Common mistakes
- Смешивать «объединение tRPC routers» с похожим механизмом без критерия выбора.
- Игнорировать риск: неверно оценить границы применения темы «объединение tRPC routers» и получить хрупкое решение.
- Показывать только синтаксис и не объяснять поведение в runtime или сборке.
What the interviewer is testing
- Объясняет композиция доменных router'ов в корневой API.
- Показывает на примере, как работает: root router собирает feature routers в дерево namespaces, а merge или nested routers должны сохранять типы без циклических зависимостей между пакетами.
- Называет production-нюанс и граничный случай для темы «объединение tRPC routers».