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».

Sources

Related topics