Разработка tRPC API для веб-приложения
tRPC (TypeScript RPC) — библиотека для построения end-to-end типизированных API без схем и кодогенерации. Типы TypeScript автоматически проходят от серверных процедур до клиентских вызовов. Работает только в экосистеме TypeScript и наиболее удобна в monorepo или fullstack-фреймворках (Next.js, Remix, SvelteKit).
Ключевая идея
// Сервер определяет процедуру
const appRouter = router({
getUser: publicProcedure
.input(z.object({ id: z.string() }))
.query(async ({ input, ctx }) => {
return ctx.db.user.findUnique({ where: { id: input.id } });
}),
});
// Клиент вызывает с полной типизацией — без кодогенерации
const user = await trpc.getUser.query({ id: 'user_123' });
// TypeScript знает тип user: { id: string; name: string; ... } | null
Нет REST-эндпоинтов, нет OpenAPI схем, нет GraphQL — просто функции с типами.
Настройка (Next.js + tRPC v11)
// server/trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { ZodError } from 'zod';
const t = initTRPC.context<Context>().create({
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError: error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
export const router = t.router;
export const publicProcedure = t.procedure;
export const protectedProcedure = t.procedure.use(({ ctx, next }) => {
if (!ctx.session?.user) throw new TRPCError({ code: 'UNAUTHORIZED' });
return next({ ctx: { ...ctx, user: ctx.session.user } });
});
Роутер и процедуры
// server/routers/articles.ts
export const articlesRouter = router({
list: publicProcedure
.input(z.object({ page: z.number().default(1), limit: z.number().max(100).default(20) }))
.query(async ({ input, ctx }) => {
const [items, total] = await ctx.db.$transaction([
ctx.db.article.findMany({ skip: (input.page - 1) * input.limit, take: input.limit }),
ctx.db.article.count(),
]);
return { items, total, pages: Math.ceil(total / input.limit) };
}),
create: protectedProcedure
.input(z.object({ title: z.string().min(1).max(200), body: z.string().min(10) }))
.mutation(async ({ input, ctx }) =>
ctx.db.article.create({ data: { ...input, authorId: ctx.user.id } })
),
delete: protectedProcedure
.input(z.string())
.mutation(async ({ input: id, ctx }) => {
const article = await ctx.db.article.findUnique({ where: { id } });
if (!article) throw new TRPCError({ code: 'NOT_FOUND' });
if (article.authorId !== ctx.user.id) throw new TRPCError({ code: 'FORBIDDEN' });
return ctx.db.article.delete({ where: { id } });
}),
});
Клиент с React Query
// Автоматическая типизация, кэшинг через React Query
function ArticleList() {
const { data, isLoading } = trpc.articles.list.useQuery({ page: 1 });
const createMutation = trpc.articles.create.useMutation({
onSuccess: () => utils.articles.list.invalidate(),
});
if (isLoading) return <Spinner />;
return (
<div>
{data?.items.map(article => <ArticleCard key={article.id} {...article} />)}
<button onClick={() => createMutation.mutate({ title: '...', body: '...' })}>
{createMutation.isPending ? 'Создаётся...' : 'Создать'}
</button>
</div>
);
}
Подписки (subscriptions)
// Сервер
articleUpdated: publicProcedure
.input(z.string())
.subscription(async function* ({ input: articleId }) {
for await (const event of eventEmitter.on(`article:${articleId}`)) {
yield event;
}
}),
// Клиент
trpc.articleUpdated.useSubscription(articleId, {
onData: (article) => setArticle(article),
});
Когда выбирать tRPC
tRPC оправдан при:
- Fullstack TypeScript monorepo (Next.js, SvelteKit, Remix)
- Небольшая команда, где фронт и бэк — один или те же разработчики
- Скорость разработки важнее межязыкового контракта
Не подходит: публичный API (нет OpenAPI), команды с разными языками на фронте и бэке, микросервисы с Go/Java бэкендом.
Сроки
tRPC API с аутентификацией, CRUD-процедурами, валидацией через Zod: 1–2 недели.







