tRPC: type-safe API без кодогенерации — полное руководство

← Назад к блогу

📑 Содержание

TL;DR: tRPC — библиотека для TypeScript, которая позволяет вызывать серверные функции с клиента с полной типобезопасностью, без REST, без GraphQL, без кодогенерации. Типы передаются через TypeScript inference — меняете сервер, и IDE мгновенно подсвечивает все несовместимости на клиенте.

1. Что такое tRPC

tRPC (TypeScript Remote Procedure Call) — это библиотека, которая позволяет создавать API как обычные TypeScript-функции. Клиент вызывает серверные процедуры напрямую, без ручного описания эндпоинтов, без HTTP-клиентов и без генерации типов из схем.

Ключевая идея

tRPC TypeScript

В классическом REST API клиент и сервер — отдельные миры. Вы описываете эндпоинты на сервере, потом вручную повторяете типы на клиенте (или генерируете их из Swagger/OpenAPI). tRPC убирает этот разрыв:

Сервер
TypeScript функция
Type Inference
автоматически
Клиент
автокомплит + проверка

Что это значит на практике

Допустим, у вас на сервере есть процедура, возвращающая пользователя:

// server: router.ts
export const appRouter = router({
  user: router({
    getById: publicProcedure
      .input(z.object({ id: z.string() }))
      .query(async ({ input }) => {
        const user = await db.user.findUnique({ where: { id: input.id } });
        return user; // { id: string, name: string, email: string }
      }),
  }),
});

На клиенте вы вызываете её так:

// client: component.tsx
const { data } = trpc.user.getById.useQuery({ id: "123" });

// data автоматически типизирован как:
// { id: string, name: string, email: string } | undefined
// IDE даёт полный автокомплит: data.name, data.email ✓
// data.phone → TS Error: Property 'phone' does not exist ✗
⚠️ Важно: tRPC работает только в TypeScript-монорепо, где клиент и сервер используют один язык. Для мультиязычных клиентов (Swift, Kotlin, Go) используйте REST или GraphQL.

2. Как работает end-to-end type safety

Магия tRPC — в TypeScript type inference. Никаких runtime-схем для передачи типов. Вот как это работает под капотом:

Цепочка вывода типов

  1. Сервер определяет процедуру с input (Zod) и output (return type)
  2. Роутер экспортирует тип: type AppRouter = typeof appRouter
  3. Клиент импортирует только тип: import type { AppRouter }
  4. TypeScript выводит типы input/output для каждой процедуры
  5. IDE подсказывает автокомплит, ловит ошибки при компиляции

Ключевое слово — import type. Это type-only import: он удаляется при компиляции и не создаёт реальной зависимости между клиентом и сервером. В бандл клиента серверный код не попадает.

// Это type-only import — удаляется при компиляции
import type { AppRouter } from '../server/router';

// Создаём типизированный клиент
import { createTRPCReact } from '@trpc/react-query';
export const trpc = createTRPCReact<AppRouter>();
💡 Без кодогенерации: В отличие от GraphQL (где нужен graphql-codegen) или OpenAPI (где нужен openapi-typescript), tRPC не генерирует файлы. Типы существуют только в TypeScript-компиляторе. Изменили сервер — клиент мгновенно видит изменения.

3. Установка и настройка

Пакеты

# Серверные пакеты
npm install @trpc/server zod

# Клиентские пакеты (React)
npm install @trpc/client @trpc/react-query @tanstack/react-query

# Для Next.js
npm install @trpc/next

Инициализация сервера

// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';

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

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

Структура проекта

my-app/
├── src/
│   ├── server/
│   │   ├── trpc.ts          # initTRPC, экспорт router/procedure
│   │   ├── context.ts       # createContext (req, session, db)
│   │   ├── router/
│   │   │   ├── index.ts     # appRouter = router({ user, post, ... })
│   │   │   ├── user.ts      # userRouter
│   │   │   └── post.ts      # postRouter
│   │   └── middleware/
│   │       └── auth.ts      # isAuthed middleware
│   ├── client/
│   │   ├── trpc.ts          # createTRPCReact<AppRouter>
│   │   └── provider.tsx     # TRPCProvider + QueryClientProvider
│   └── pages/ or app/       # Next.js pages/app directory
├── package.json
└── tsconfig.json

4. Роутеры и процедуры

tRPC строится вокруг двух концепций: роутеры (группируют процедуры) и процедуры (серверные функции).

Типы процедур

Тип HTTP-метод Назначение Аналог REST
query GET Чтение данных GET /users/:id
mutation POST Создание / изменение / удаление POST, PUT, DELETE
subscription WebSocket Реал-тайм подписки WebSocket / SSE

Пример: CRUD-роутер для постов

// server/router/post.ts
import { z } from 'zod';
import { router, publicProcedure, protectedProcedure } from '../trpc';

export const postRouter = router({
  // Query — получение списка
  list: publicProcedure
    .input(z.object({
      limit: z.number().min(1).max(100).default(20),
      cursor: z.string().nullish(),
    }))
    .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' },
        include: { author: { select: { name: true, image: true } } },
      });

      let nextCursor: string | undefined;
      if (posts.length > input.limit) {
        const next = posts.pop();
        nextCursor = next?.id;
      }

      return { posts, nextCursor };
    }),

  // 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 },
        include: { author: true, comments: true },
      });

      if (!post) {
        throw new TRPCError({ code: 'NOT_FOUND', message: 'Post not found' });
      }

      return post;
    }),

  // Mutation — создание поста (только авторизованные)
  create: protectedProcedure
    .input(z.object({
      title: z.string().min(1).max(200),
      content: z.string().min(1),
      published: z.boolean().default(false),
    }))
    .mutation(async ({ input, ctx }) => {
      return ctx.db.post.create({
        data: { ...input, authorId: ctx.session.user.id },
      });
    }),

  // Mutation — удаление
  delete: protectedProcedure
    .input(z.object({ id: z.string().uuid() }))
    .mutation(async ({ input, ctx }) => {
      const post = await ctx.db.post.findUnique({ where: { id: input.id } });

      if (post?.authorId !== ctx.session.user.id) {
        throw new TRPCError({ code: 'FORBIDDEN' });
      }

      return ctx.db.post.delete({ where: { id: input.id } });
    }),
});

Сборка роутеров

// server/router/index.ts
import { router } from '../trpc';
import { userRouter } from './user';
import { postRouter } from './post';
import { commentRouter } from './comment';

export const appRouter = router({
  user: userRouter,
  post: postRouter,
  comment: commentRouter,
});

// Экспорт типа — это всё, что нужно клиенту
export type AppRouter = typeof appRouter;

5. Валидация с Zod

Zod — стандарт валидации в tRPC. Он выполняет двойную роль: валидирует данные в runtime и выводит TypeScript-типы.

Zod + tRPC = полная безопасность

Zod TypeScript

Практические примеры схем

import { z } from 'zod';

// Переиспользуемые схемы
const emailSchema = z.string().email('Некорректный email');
const passwordSchema = z.string()
  .min(8, 'Минимум 8 символов')
  .regex(/[A-Z]/, 'Нужна заглавная буква')
  .regex(/[0-9]/, 'Нужна цифра');

// Схема регистрации
const registerInput = z.object({
  email: emailSchema,
  password: passwordSchema,
  name: z.string().min(2).max(50),
  role: z.enum(['user', 'admin']).default('user'),
});

// Схема с трансформацией
const createPostInput = z.object({
  title: z.string().min(1).max(200).transform(s => s.trim()),
  content: z.string().min(1),
  tags: z.array(z.string()).max(10).default([]),
  publishAt: z.string().datetime().optional(),
});

// Пагинация (переиспользуемая)
const paginationInput = z.object({
  page: z.number().int().positive().default(1),
  perPage: z.number().int().min(1).max(100).default(20),
  sortBy: z.string().optional(),
  sortOrder: z.enum(['asc', 'desc']).default('desc'),
});

// Использование в процедуре
const userRouter = router({
  register: publicProcedure
    .input(registerInput)
    .mutation(async ({ input }) => {
      // input уже типизирован И провалидирован
      // input.email — string (валидный email)
      // input.role — 'user' | 'admin'
    }),

  list: protectedProcedure
    .input(paginationInput)
    .query(async ({ input }) => {
      // input.page — number, input.perPage — number
    }),
});
⚠️ Output-валидация: tRPC v11 поддерживает .output() для валидации ответа. Это полезно, чтобы не утечь лишним данным (например, password hash):
const userPublicSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string(),
  createdAt: z.date(),
});

getById: publicProcedure
  .input(z.object({ id: z.string() }))
  .output(userPublicSchema) // strip лишних полей
  .query(async ({ input, ctx }) => {
    return ctx.db.user.findUniqueOrThrow({ where: { id: input.id } });
  }),

6. Context и Middleware

Context — данные запроса

Context создаётся для каждого запроса и содержит общие зависимости: подключение к БД, сессию пользователя, логгер.

// server/context.ts
import { type inferAsyncReturnType } from '@trpc/server';
import { type FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch';
import { getServerSession } from 'next-auth';
import { prisma } from './db';

export async function createContext(opts: FetchCreateContextFnOptions) {
  const session = await getServerSession();

  return {
    db: prisma,
    session,
    headers: opts.req.headers,
  };
}

export type Context = inferAsyncReturnType<typeof createContext>;

Middleware — переиспользуемая логика

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

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

  return next({
    ctx: {
      session: ctx.session, // теперь session гарантированно не null
    },
  });
});

// Middleware для логирования
const logger = t.middleware(async ({ path, type, next }) => {
  const start = Date.now();
  const result = await next();
  const duration = Date.now() - start;

  console.log(`${type} ${path} — ${duration}ms`);
  return result;
});

// Middleware для rate limiting
const rateLimit = t.middleware(async ({ ctx, next }) => {
  const ip = ctx.headers.get('x-forwarded-for') ?? 'unknown';
  const allowed = await checkRateLimit(ip);

  if (!allowed) {
    throw new TRPCError({
      code: 'TOO_MANY_REQUESTS',
      message: 'Rate limit exceeded',
    });
  }

  return next();
});

// Комбинирование middleware
export const protectedProcedure = t.procedure
  .use(logger)
  .use(rateLimit)
  .use(isAuthed);
💡 Типобезопасный context: Middleware может расширять ctx. После isAuthed TypeScript знает, что ctx.session — не null. Это избавляет от проверок if (!session) в каждой процедуре.

7. Клиент: React Query + tRPC

tRPC использует TanStack Query (React Query) как транспортный слой на клиенте. Вы получаете кеширование, рефетч, оптимистичные обновления — всё из коробки.

Настройка провайдера

// client/provider.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { useState } from 'react';
import { trpc } from './trpc';

export function TRPCProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 5 * 60 * 1000, // 5 минут
        retry: 1,
      },
    },
  }));

  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: '/api/trpc',
          headers() {
            return {
              'x-trpc-source': 'react',
            };
          },
        }),
      ],
    }),
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </trpc.Provider>
  );
}

Использование в компонентах

// components/PostList.tsx
import { trpc } from '@/client/trpc';

export function PostList() {
  // Query — автокомплит по пути: trpc.post.list
  const { data, isLoading, fetchNextPage, hasNextPage } =
    trpc.post.list.useInfiniteQuery(
      { limit: 20 },
      { getNextPageParam: (lastPage) => lastPage.nextCursor },
    );

  // Mutation с оптимистичным обновлением
  const utils = trpc.useUtils();
  const deleteMutation = trpc.post.delete.useMutation({
    onMutate: async ({ id }) => {
      await utils.post.list.cancel();
      const prev = utils.post.list.getInfiniteData({ limit: 20 });

      utils.post.list.setInfiniteData({ limit: 20 }, (old) => {
        if (!old) return old;
        return {
          ...old,
          pages: old.pages.map((page) => ({
            ...page,
            posts: page.posts.filter((p) => p.id !== id),
          })),
        };
      });

      return { prev };
    },
    onError: (_err, _vars, context) => {
      utils.post.list.setInfiniteData({ limit: 20 }, context?.prev);
    },
    onSettled: () => {
      utils.post.list.invalidate();
    },
  });

  if (isLoading) return <div>Загрузка...</div>;

  return (
    <div>
      {data?.pages.map((page) =>
        page.posts.map((post) => (
          <article key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.author.name}</p>
            <button onClick={() => deleteMutation.mutate({ id: post.id })}>
              Удалить
            </button>
          </article>
        )),
      )}
      {hasNextPage && (
        <button onClick={() => fetchNextPage()}>Загрузить ещё</button>
      )}
    </div>
  );
}

Prefetch и SSR

// Prefetch на сервере (Next.js App Router)
import { createServerSideHelpers } from '@trpc/react-query/server';

export default async function PostPage({ params }: { params: { id: string } }) {
  const helpers = createServerSideHelpers({
    router: appRouter,
    ctx: await createContext(),
  });

  // Данные загружаются на сервере
  await helpers.post.getById.prefetch({ id: params.id });

  return (
    <HydrationBoundary state={helpers.dehydrate()}>
      <PostContent id={params.id} />
    </HydrationBoundary>
  );
}

8. Обработка ошибок

tRPC предоставляет типизированные ошибки, которые маппятся на HTTP-статусы:

tRPC Code HTTP Status Когда использовать
BAD_REQUEST400Невалидные данные (Zod ошибки)
UNAUTHORIZED401Нет аутентификации
FORBIDDEN403Нет прав доступа
NOT_FOUND404Ресурс не найден
CONFLICT409Конфликт (дубликат)
TOO_MANY_REQUESTS429Превышен rate limit
INTERNAL_SERVER_ERROR500Серверная ошибка

Бросание ошибок на сервере

import { TRPCError } from '@trpc/server';

create: protectedProcedure
  .input(createUserInput)
  .mutation(async ({ input, ctx }) => {
    const existing = await ctx.db.user.findUnique({
      where: { email: input.email },
    });

    if (existing) {
      throw new TRPCError({
        code: 'CONFLICT',
        message: 'Пользователь с таким email уже существует',
        cause: { field: 'email' },
      });
    }

    return ctx.db.user.create({ data: input });
  }),

Обработка ошибок на клиенте

import { TRPCClientError } from '@trpc/client';

const mutation = trpc.user.create.useMutation({
  onError: (error) => {
    if (error instanceof TRPCClientError) {
      // Типизированная ошибка
      if (error.data?.code === 'CONFLICT') {
        toast.error('Email уже занят');
        return;
      }

      // Zod-ошибки валидации
      if (error.data?.zodError) {
        const fieldErrors = error.data.zodError.fieldErrors;
        // fieldErrors.email → ['Некорректный email']
        setErrors(fieldErrors);
        return;
      }
    }

    toast.error('Что-то пошло не так');
  },
});
💡 Error formatting: Настройте errorFormatter при создании tRPC, чтобы Zod-ошибки автоматически передавались клиенту в удобном формате. Подробнее о дебаге API-ошибок — в нашей статье «Чеклист отладки API-ошибок».

9. tRPC + Next.js (App Router)

tRPC и Next.js — каноническая пара. В Next.js 14+ (App Router) tRPC можно использовать как в Server Components, так и в Client Components.

API Route Handler

// app/api/trpc/[trpc]/route.ts
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '@/server/router';
import { createContext } from '@/server/context';

const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: () => createContext({ req }),
  });

export { handler as GET, handler as POST };

Server Components (RSC) — без HTTP

// app/posts/page.tsx (Server Component)
import { createCaller } from '@/server/router';
import { createContext } from '@/server/context';

export default async function PostsPage() {
  const ctx = await createContext();
  const caller = createCaller(ctx);

  // Прямой вызов — без HTTP, без сериализации
  const posts = await caller.post.list({ limit: 10 });

  return (
    <div>
      {posts.posts.map((post) => (
        <article key={post.id}>
          <h2>{post.title}</h2>
        </article>
      ))}
    </div>
  );
}

T3 Stack

Next.js tRPC Prisma NextAuth

T3 Stack — самый популярный full-stack TypeScript стартер. Включает:

npm create t3-app@latest

10. tRPC vs REST vs GraphQL

REST

Стандарт индустрии

Универсальность Простота Типобезопасность Over/under-fetching

GraphQL

Гибкие запросы

Гибкость Мультиязычность Сложность Кодогенерация

tRPC

Type-safe из коробки

Типобезопасность DX Простота Только TypeScript
Критерий REST GraphQL tRPC
Типобезопасность ❌ Ручная / codegen ⚠️ Codegen (graphql-codegen) ✅ Из коробки (inference)
Кодогенерация Нет / openapi-ts Обязательна Не нужна
Языки клиентов Любые Любые Только TypeScript
Кривая обучения Низкая Высокая Низкая (знание TS)
Over-fetching Часто Решено Зависит от процедур
Caching HTTP-кеш Сложный React Query
Документация Swagger / OpenAPI Introspection TypeScript = документация
Публичное API ✅ Стандарт ✅ Подходит ❌ Не подходит
Batching Нет (по умолчанию) Да Да (httpBatchLink)
💡 Когда какой выбрать:
REST — публичное API, мультиязычные клиенты, простые CRUD.
GraphQL — сложные запросы, мобильные + веб + desktop клиенты на разных языках.
tRPC — full-stack TypeScript (Next.js, T3), внутренние сервисы, стартапы.

11. Паттерны и best practices

1. Batching запросов

tRPC по умолчанию батчит запросы через httpBatchLink. Несколько вызовов, сделанных в одном рендере, объединяются в один HTTP-запрос:

// Эти 3 вызова → 1 HTTP-запрос
const user = trpc.user.getById.useQuery({ id: '1' });
const posts = trpc.post.list.useQuery({ limit: 10 });
const stats = trpc.analytics.summary.useQuery();

// Batched request:
// POST /api/trpc/user.getById,post.list,analytics.summary

2. Переиспользование input-схем

// shared/schemas.ts — общие схемы
export const idInput = z.object({ id: z.string().uuid() });

export const paginatedInput = z.object({
  limit: z.number().min(1).max(100).default(20),
  cursor: z.string().nullish(),
});

export const searchInput = paginatedInput.extend({
  query: z.string().min(1).max(200),
});

// Используем в разных роутерах
getById: publicProcedure.input(idInput).query(/* ... */),
search: publicProcedure.input(searchInput).query(/* ... */),

3. Подписки (WebSocket)

// server/router/chat.ts
import { observable } from '@trpc/server/observable';

export const chatRouter = router({
  onMessage: publicProcedure
    .input(z.object({ roomId: z.string() }))
    .subscription(({ input }) => {
      return observable<{ text: string; author: string }>((emit) => {
        const handler = (msg: Message) => {
          if (msg.roomId === input.roomId) {
            emit.next({ text: msg.text, author: msg.author });
          }
        };

        eventEmitter.on('message', handler);
        return () => eventEmitter.off('message', handler);
      });
    }),
});

// client
const { data } = trpc.chat.onMessage.useSubscription(
  { roomId: '123' },
  {
    onData: (message) => {
      // message типизирован: { text: string, author: string }
      addMessage(message);
    },
  },
);

4. trpc-openapi — REST из tRPC

Если нужно предоставить REST API наряду с tRPC (для внешних клиентов), используйте trpc-openapi:

import { generateOpenApiDocument } from 'trpc-openapi';

// Помечаем процедуры для REST
getById: publicProcedure
  .meta({ openapi: { method: 'GET', path: '/users/{id}' } })
  .input(z.object({ id: z.string() }))
  .output(userSchema)
  .query(/* ... */),

// Генерация OpenAPI-схемы
const openApiDoc = generateOpenApiDocument(appRouter, {
  title: 'My API',
  version: '1.0.0',
  baseUrl: 'https://api.example.com',
});
💡 Гибридный подход: Используйте tRPC для внутренних клиентов (Next.js фронтенд) и trpc-openapi для внешних (Swagger-документация, мобильные приложения на Swift/Kotlin).

5. Тестирование

// __tests__/post.test.ts
import { createCaller } from '@/server/router';

describe('post router', () => {
  it('creates a post', async () => {
    const caller = createCaller({
      db: prismaMock,
      session: { user: { id: 'user-1' } },
    });

    const post = await caller.post.create({
      title: 'Test Post',
      content: 'Content here',
    });

    expect(post.title).toBe('Test Post');
    expect(post.authorId).toBe('user-1');
  });

  it('throws UNAUTHORIZED for unauthenticated user', async () => {
    const caller = createCaller({ db: prismaMock, session: null });

    await expect(
      caller.post.create({ title: 'Test', content: 'Content' }),
    ).rejects.toThrow('UNAUTHORIZED');
  });

  it('validates input with Zod', async () => {
    const caller = createCaller({
      db: prismaMock,
      session: { user: { id: 'user-1' } },
    });

    await expect(
      caller.post.create({ title: '', content: 'Content' }),
    ).rejects.toThrow(); // Zod: "String must contain at least 1 character"
  });
});

12. Когда НЕ использовать tRPC

🚫 tRPC не подходит, если:

✅ Используйте tRPC, если

  • Full-stack TypeScript (Next.js, T3)
  • Монорепо (клиент + сервер)
  • Нужен максимальный DX
  • Стартап / внутренний продукт
  • React / React Native фронтенд
  • Нужна скорость разработки

❌ Используйте REST/GraphQL, если

  • Публичное API для сторонних разработчиков
  • Клиенты на разных языках
  • Бэкенд не на TypeScript
  • Нужна Swagger/OpenAPI документация
  • Микросервисная архитектура (разные языки)
  • Нужна обратная совместимость

FAQ

Что такое tRPC и зачем он нужен?

tRPC — библиотека для создания type-safe API на TypeScript без кодогенерации. Обеспечивает end-to-end типобезопасность: типы с сервера автоматически доступны на клиенте через TypeScript inference. Идеален для full-stack TypeScript проектов (Next.js, T3 Stack).

Можно ли использовать tRPC с мобильными приложениями?

Да — с React Native через @trpc/react-query. Для нативных приложений (Swift, Kotlin) tRPC не подходит напрямую. Используйте trpc-openapi для генерации REST-эндпоинтов из tRPC-роутеров, или выберите REST API / GraphQL.

Чем tRPC отличается от GraphQL?

GraphQL требует SDL-схемы и кодогенерации, работает с любыми языками. tRPC использует TypeScript inference — типы выводятся автоматически, без генерации файлов. tRPC проще для TypeScript-монорепо. GraphQL лучше для мультиязычных проектов и публичных API.

Подходит ли tRPC для production?

Да. tRPC используется в production многими компаниями. Библиотека стабильна (v11), имеет активное сообщество. tRPC — основа T3 Stack. Cal.com, Ping.gg и другие крупные проекты работают на tRPC.

Как tRPC работает с идемпотентностью?

tRPC-мутации — обычные POST-запросы. Для идемпотентности добавьте middleware с Idempotency-Key заголовком, как и в REST API. Или реализуйте через Zod-схему с idempotencyKey полем.

Можно ли добавить tRPC к существующему REST API?

Да. tRPC может работать параллельно с REST: разные route handlers обрабатывают разные пути. Миграция может быть постепенной — новые эндпоинты на tRPC, старые остаются на REST.

Полезные ссылки

🚀 Прототипируйте API быстрее

LightBox API — создавайте моковые эндпоинты для тестирования фронтенда, пока бэкенд на tRPC ещё в разработке.

Попробовать бесплатно →

Статья опубликована: 23 февраля 2026
Автор: LightBox API Team