tRPCMiddleCoding
Что такое router в tRPC и как определять процедуры (procedures)?
Router в tRPC — это контейнер для объединения процедур (query, mutation, subscription) в именованные группы. Процедуры определяются цепочкой: input() задаёт Zod-схему входных данных, query()/mutation() — реализацию обработчика.
Что такое router в tRPC
Router — это объект, который объединяет набор процедур под общим пространством имён. Роутеры можно вкладывать друг в друга для создания иерархической структуры API. Итоговый appRouter экспортируется с сервера и используется как единая точка входа для всего API.
Инициализация tRPC
// server/trpc/init.ts
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';
import type { Context } from './context';
// Создаём экземпляр tRPC с типом контекста
export const t = initTRPC.context<Context>().create({
transformer: superjson,
});
// Базовые строительные блоки
export const router = t.router;
export const publicProcedure = t.procedure;
Определение процедур
Процедура строится цепочкой методов. Порядок важен: сначала .input() для валидации, потом .query(), .mutation() или .subscription() для реализации.
// server/trpc/routers/post.ts
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
import { router, publicProcedure } from '../init';
export const postRouter = router({
// Query: получение данных
getById: publicProcedure
.input(z.object({ id: z.string().uuid() }))
.query(async ({ input, ctx }) => {
const post = await ctx.db.post.findUnique({
where: { id: input.id },
});
if (!post) {
throw new TRPCError({
code: 'NOT_FOUND',
message: `Post ${input.id} not found`,
});
}
return post;
}),
// Query без входных параметров
list: publicProcedure
.input(z.object({
limit: z.number().int().min(1).max(100).default(20),
cursor: z.string().uuid().optional(),
}))
.query(async ({ input, ctx }) => {
const posts = await ctx.db.post.findMany({
take: input.limit + 1,
cursor: input.cursor ? { id: input.cursor } : undefined,
orderBy: { createdAt: 'desc' },
});
const hasMore = posts.length > input.limit;
return {
items: hasMore ? posts.slice(0, -1) : posts,
nextCursor: hasMore ? posts[posts.length - 1].id : null,
};
}),
// Mutation: изменение данных
create: publicProcedure
.input(z.object({
title: z.string().min(1).max(200),
content: z.string(),
published: z.boolean().default(false),
}))
.mutation(async ({ input, ctx }) => {
return ctx.db.post.create({ data: input });
}),
// Mutation без входных параметров (удаление по ID)
delete: publicProcedure
.input(z.object({ id: z.string().uuid() }))
.mutation(async ({ input, ctx }) => {
await ctx.db.post.delete({ where: { id: input.id } });
return { success: true };
}),
});
Композиция роутеров
// server/trpc/router.ts
import { router } from './init';
import { postRouter } from './routers/post';
import { userRouter } from './routers/user';
import { commentRouter } from './routers/comment';
export const appRouter = router({
post: postRouter, // доступен как trpc.post.getById
user: userRouter, // доступен как trpc.user.getProfile
comment: commentRouter, // доступен как trpc.comment.create
});
export type AppRouter = typeof appRouter;
Защищённые процедуры через middleware
// server/trpc/init.ts (расширение)
import { TRPCError } from '@trpc/server';
const isAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.userId) {
throw new TRPCError({ code: 'UNAUTHORIZED' });
}
return next({ ctx: { ...ctx, userId: ctx.userId } });
});
export const protectedProcedure = t.procedure.use(isAuthed);
Подводные камни
- Забытый
.input()перед.query()приводит к тому, что процедура принимаетundefined— входные данные не валидируются, TypeScript не предупредит. - Нельзя вызывать
router()внутри другого роутера динамически (в цикле или условно) — структура должна быть статической для корректного вывода типов. - Имена процедур регистрозависимы:
getByIdиgetbyid— разные ключи. Используйте единый стиль именования (camelCase). - Выброс любого исключения, кроме
TRPCError, приводит к ответу с кодомINTERNAL_SERVER_ERRORи скрытым сообщением — всегда оборачивайте ошибки вTRPCError. - Слишком большой плоский роутер (все процедуры в одном объекте) ухудшает навигацию и увеличивает время компиляции — разбивайте на модульные sub-роутеры.
- При использовании
mergeRoutersколлизии имён верхнего уровня приводят к молчаливому переопределению — следите за уникальностью ключей.
Common mistakes
- Смешивать «router и procedures» с похожим механизмом без критерия выбора.
- Игнорировать риск: неверно оценить границы применения темы «router и procedures» и получить хрупкое решение.
- Показывать только синтаксис и не объяснять поведение в runtime или сборке.
What the interviewer is testing
- Объясняет организация API в дерево процедур
query,mutationиsubscription. - Показывает на примере, как работает: router группирует процедуры и вложенные router'ы, а procedure задает тип операции, input validation, middleware и resolver.
- Называет production-нюанс и граничный случай для темы «router и procedures».