tRPC
tRPC позволяет разрабатывать полностью безопасные с точки зрения типов API для клиент-серверных приложений (предпочтительной является архитектура монорепозитория). Это посредник между сервером и клиентом, позволяющий им использовать один маршрутизатор (роутер) для обработки запросов HTTP. Использование одного роутера, в свою очередь, обуславливает возможность автоматического вывода типов (type inference) входящих и исходящих данных (input/output), что особенно актуально для клиента и позволяет избежать дублирования типов или использования общих (shared) типов.
tRPC состоит из нескольких отдельных пакетов, основные из которых рассматриваются ниже.
@trpc/server
npm i @trpc/server
# или
yarn add @trpc/server
Определение роутеров / routers
Разработка интерфейса (API), основанного на tRPC, начинается с определения роутера.
Инициализация tPRC
Важно: tRPC должен инициализироваться только один раз.
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const router = t.router;
export const middleware = t.middleware;
export const publicProcedure = t.procedure;
Определение роутера
Определяем конечную точку интерфейса:
import * as trpc from '@trpc/server';
import { publicProcedure, router } from './trpc';
const appRouter = router({
greeting: publicProcedure.query(() => 'hello tRPC v10!'),
});
// Экспортируем только тип маршрутизатора!
// Это предотвращает и мпорт серверного кода на клиенте
export type AppRouter = typeof appRouter;
Продвинутое использование
При инициализации маршрутизатора можно делать следующее:
- настраивать контексты запросов
- добавлять метаданные в процедуры
- форматировать и обрабатывать ошибки
- преобразовывать данные
- кастомизировать настройку выполнения кода (runtime)
Для кастомизации объекта t
при ин ициализации может использоваться цепочка методов, например:
import { Context, Meta } from '@trpc/server';
const t = initTRPC.context<Context>().meta<Meta>().create({
// ...
});
Настройка выполнения кода
export interface RuntimeConfig<TTypes extends RootConfigTypes> {
/**
* Преобразователь данных
* @link https://trpc.io/docs/data-transformers
*/
transformer: TTypes['transformer'];
/**
* Редактор ошибок
* @link https://trpc.io/docs/error-formatting
*/
errorFormatter: ErrorFormatter<TTypes['ctx'], any>;
/**
* Позволяет запускать `@trpc/server` в несерверных окружениях,
* например, в тестах
* @default false
*/
allowOutsideOfServer: boolean;
/**
* Индикатор выполнения кода на сервере
* @default typeof window === 'undefined' || 'Deno' in window || process.env.NODE_ENV === 'test'
*/
isServer: boolean;
/**
* Индикатор выполнения кода в режиме разработки
* Используется для определения необходимости возврата трассировки стека
* @default process.env.NODE_ENV !== 'production'
*/
isDev: boolean;
}
Определение процедур / procedures
Процедура - это очень гибкий примитив для создания серверных функций. Для создания процедуры используется паттерн "Строитель" (builder), что позволяет определять переиспользуемые (reusable) процедуры для разных частей сервера.
Пример без валидации входных данных
import { router, publicProcedure } from './trpc';
import { z } from 'zod';
const appRouter = router({
// Создаем открытую процедуру для пути `hello`
hello: publicProcedure.query(() => {
return {
greeting: 'всем привет',
};
}),
});
Валидация входных данных
tRPC поддерживает yup/superstruct/zod/myzod/кастомные валидаторы. Пример использования Zod:
import { publicProcedure, router } from './trpc';
import { z } from 'zod';
export const appRouter = router({
hello: publicProcedure
// Входные данные должны быть опциональным объектом,
// содержащий поле `name` со строковым значением
.input(
z
.object({
name: z.string(),
})
.optional(),
)
.query(({ input }) => {
return {
greeting: `привет, ${input?.name ?? 'народ'}`,
};
}),
});
export type AppRouter = typeof appRouter;
Несколько валидаторов входных данных
Валидаторы могут "выстраиваться" в цепочку:
// @filename: roomProcedure.ts
import { publicProcedure } from './trpc';
import { z } from 'zod';
/**
* Создаем переиспользуемую открытую процедуру для комнаты чата
*/
export const roomProcedure = publicProcedure.input(
z.object({
roomId: z.string(),
}),
);
// @filename: _app.ts
import { router } from './trpc';
import { roomProcedure } from './roomProcedure';
import { z } from 'zod';
const appRouter = router({
sendMessage: roomProcedure
// Добавляем дополнительную валидацию входных данных для процедуры `sendMessage`
.input(
z.object({
text: z.string(),
}),
)
.mutation(({ input }) => {
// ...
}),
});
export type AppRouter = typeof appRouter;
Несколько процедур
Для добавления нескольких процедур достаточно определить их в виде полей объекта, передаваемого в t.router()
:
import { initTRPC } from '@trpc/server';
export const t = initTRPC.create();
const router = t.router;
const publicProcedure = t.procedure;
export const appRouter = router({
hi: publicProcedure.query(() => {
return {
text: 'всем привет',
};
}),
bye: publicProcedure.query(() => {
return {
text: 'всем пока',
};
}),
});
export type AppRouter = typeof appRouter;
Переиспользуемые базовые процедуры
Пример создания набора защищенных процедур авторизации для приложения Next.js:
// @filename: context.ts
import { inferAsyncReturnType } from '@trpc/server';
import * as trpcNext from '@trpc/server/adapters/next';
import { getSession } from 'next-auth/react';
/**
* Создаем контекст для входящего запроса
* @link https://trpc.io/docs/context
*/
export async function createContext(opts: trpcNext.CreateNextContextOptions) {
const session = await getSession({ req: opts.req });
return {
session,
};
};
export type Context = inferAsyncReturnType<typeof createContext>;
// @filename: trpc.ts
import { initTRPC, TRPCError } from '@trpc/server';
import { Context } from './context';
const t = initTRPC.context<Context>().create();
/**
* Переиспо льзуемый посредник, проверяющий состояние аутентификации пользователя
**/
const isAuthed = t.middleware(({ next, ctx }) => {
if (!ctx.session?.user?.email) {
throw new TRPCError({
code: 'UNAUTHORIZED',
});
}
return next({
ctx: {
// `session` имеет ненулевое значение (non-nullable)
session: ctx.session,
},
});
});
export const middleware = t.middleware;
export const router = t.router;
/**
* Открытая процедура
**/
export const publicProcedure = t.procedure;
/**
* Защищенная процедура
**/
export const protectedProcedure = t.procedure.use(isAuthed);
// @filename: _app.ts
import { protectedProcedure, publicProcedure, router } from './trpc';
import { z } from 'zod';
export const appRouter = router({
createPost: protectedProcedure
.mutation(({ ctx }) => {
const session = ctx.session;
// ...
}),
whoami: publicProcedure
.query(({ ctx }) => {
const session = ctx.session;
// ...
}),
});
Объединение роутеров
Объединение дочерних роутеров
// @filename: trpc.ts
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
export const middleware = t.middleware;
export const router = t.router;
export const publicProcedure = t.procedure;
// @filename: routers/_app.ts
import { router } from '../trpc';
import { z } from 'zod';
import { userRouter } from './user';
import { postRouter } from './post';
const appRouter = router({
user: userRouter, // процедуры пространства (namespace) `user`
post: postRouter, // процедуры пространства `post`
});
export type AppRouter = typeof appRouter;
// @filename: routers/post.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
export const postRouter = router({
create: publicProcedure
.input(
z.object({
title: z.string(),
}),
)
.mutation(({ input }) => {
// ...
}),
list: publicProcedure.query(() => {
// ...
return [];
}),
});
// @filename: routers/user.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
export const userRouter = router({
list: publicProcedure.query(() => {
// ...
return [];
}),
});
Объединение с помощью t.mergeRouters()
Метод t.mergeRouters
позволяет объединять роутеры в одно пространство:
// @filename: routers/_app.ts
import { router, publicProcedure, mergeRouters } from '../trpc';
import { z } from 'zod';
import { userRouter } from './user';
import { postRouter } from './post';
const appRouter = mergeRouters(userRouter, postRouter)
export type AppRouter = typeof appRouter;
// @filename: routers/post.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
export const postRouter = router({
postCreate: publicProcedure
.input(
z.object({
title: z.string(),
}),
)
.mutation(({ input }) => {
// ...
}),
postList: publicProcedure.query(() => {
// ...
return [];
}),
});
// @filename: routers/user.ts
import { router, publicProcedure } from '../trpc';
import { z } from 'zod';
export const userRouter = router({
userList: publicProcedure.query(() => {
// ...
return [];
}),
});
Контекст / context
Контекст содержит данные для процедур, такие как соединение с базой данных или информация об аутентификации.
Установка контекста состоит из 2 шагов: определение типа при инициализации и создание контекста выполнения для каждого запроса.
Определение типа контекста
Тип контекста определяется с помощью initTRPC.context<TContext>()
перед .create()
. Тип TContext
может выводиться из типа возвращаемого функцией значения или определяться явно.
import { initTRPC, type inferAsyncReturnType } from '@trpc/server';
import type { CreateNextContextOptions } from '@trpc/server/adapters/next';
import { getSession } from 'next-auth/react';
export const createContext = async (opts: CreateNextContextOptions) => {
const session = await getSession({ req: opts.req });
return {
session,
};
};
const t1 = initTRPC.context<typeof createContext>().create();
t1.procedure.use(({ ctx }) => {
// ...
});
type Context = inferAsyncReturnType<typeof createContext>;
const t2 = initTRPC.context<Context>().create();
t2.procedure.use(({ ctx }) => {
// ...
});