tRPCMiddleExperience

Расскажите о случае, когда вы улучшали performance, accessibility, testing или maintainability в проекте на tRPC.

Пример из практики: добавление Zod-валидации на все процедуры сразу через middleware улучшило maintainability; подключение superjson устранило баги с Date-сериализацией; покрытие API-слоя интеграционными тестами через createCallerFactory дало уверенность при рефакторинге.

Контекст задачи

В одном из проектов tRPC-бэкенд вырос органически: около 40 процедур, разбросанных по 8 роутерам. Со временем накопились проблемы: дублирование логики валидации, медленные тесты из-за реальных HTTP-запросов, отсутствие обработки ошибок для клиента, и утечки типов Date в JSON.

Что улучшили и как

1. Maintainability: вынос общей логики в middleware

Повторяющаяся проверка аутентификации была скопирована в каждую защищённую процедуру. Вынесли её в reusable middleware через t.middleware и создали protectedProcedure:

// server/trpc/middleware.ts
import { TRPCError } from '@trpc/server';
import { t } from './init';

const isAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.session?.userId) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return next({ ctx: { ...ctx, userId: ctx.session.userId } });
});

export const protectedProcedure = t.procedure.use(isAuthed);

После этого каждый роутер просто импортирует protectedProcedure вместо дублирования условия.

2. Testing: createCallerFactory для быстрых интеграционных тестов

Тесты через HTTP занимали 3–5 с на старт. Переписали на прямые вызовы:

// tests/user.test.ts
import { createCallerFactory } from '@trpc/server';
import { appRouter } from '../server/trpc/router';
import { createMockContext } from './helpers';

const createCaller = createCallerFactory(appRouter);

test('user.getById returns correct user', async () => {
  const ctx = createMockContext({ userId: 'u1' });
  const caller = createCaller(ctx);
  const user = await caller.user.getById({ id: 'u1' });
  expect(user.name).toBe('Alice');
});

Время прогона 40 тестов сократилось с 18 с до 1.2 с — нет реального HTTP, нет запуска сервера.

3. Корректная сериализация: подключение superjson

Клиент получал даты как строки вместо Date-объектов, что вызывало баги в компонентах с форматированием. Подключили трансформер:

// server/trpc/init.ts
import { initTRPC } from '@trpc/server';
import superjson from 'superjson';

export const t = initTRPC.context<Context>().create({
  transformer: superjson,
});
// lib/trpc.ts (клиент)
import { createTRPCReact } from '@trpc/react-query';
import superjson from 'superjson';
import { httpBatchLink } from '@trpc/client';
import type { AppRouter } from '../server/trpc/router';

export const trpc = createTRPCReact<AppRouter>();

export const trpcClient = trpc.createClient({
  links: [
    httpBatchLink({
      url: '/api/trpc',
      transformer: superjson,
    }),
  ],
});

4. Accessibility ошибок: форматированный errorFormatter

Zod-ошибки валидации возвращались как непонятный JSON. Добавили errorFormatter, который возвращает поле fieldErrors для форм:

export const t = initTRPC.context<Context>().create({
  transformer: superjson,
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        fieldErrors:
          error.cause instanceof ZodError
            ? error.cause.flatten().fieldErrors
            : null,
      },
    };
  },
});

Подводные камни

  • Middleware применяется цепочкой — порядок вызова .use() имеет значение: логирование должно идти до проверки авторизации, иначе неавторизованные запросы не логируются.
  • createCallerFactory не проходит через HTTP-слой — заголовки, cookies и rate-limiting не тестируются этим способом. Для end-to-end нужен отдельный слой тестов.
  • При добавлении superjson после запуска в продакшене старые клиенты (без трансформера) перестанут корректно парсить ответы — нужен координированный деплой.
  • Refactoring роутеров с переименованием процедур ломает клиентский код сразу — это полезно, но нужно учитывать при CI: TypeScript должен проверять весь monorepo.
  • Глубокое вложение middleware увеличивает время вывода типов TypeScript — разбивайте сложные цепочки на именованные переменные.

What hurts your answer

  • Выдумывать опыт или говорить слишком общими фразами
  • Не объяснять свою личную роль в работе с tRPC
  • Не показывать результат, метрики или извлечённые уроки

What they're listening for

  • Может подготовить честный пример использования tRPC
  • Показывает свою роль, решения и результат
  • Умеет рефлексировать над trade-offs и уроками

Related topics