Наш фронтендер Сергей вошел в сумеречную зону между бэкэндом и фронтендом, чтобы выяснить, как можно устранить рассинхронизацию контрактов API, выжать из OpenAPI еще больше пользы и минимизировать человеческий фактор с помощью библиотеки TRPC.
Причины, по которым type safety важна лично для меня, можно просуммировать в одну — меньше общения на неприятные темы, вроде, «кто закосячил и кому теперь разгребать последствия данных косяков». Многолетний тренд движения фронтенд-кода к большей type safety показывает, что не только я задумываюсь об этом.
Глоссарий
- Тип - абстракция, объединяющая значения по общности неких признаков.
- Турe safety (тайпсейфти) - соответствие типов, вычисленных во время компиляции, значениям во время выполнения [программы].
- End-to-end - на всем протяжении [пути данных от и до конечного пользователя].
- Кодогенерация - генерация кода для кодовой базы посредством исполнения какой-то программы (create-t3-app, например).
Open API значительно сокращает монотонный и не всегда приятный ручной труд, который обычно сопровождает разработку.
Погружаясь в тему end-to-end type safety, я столкнулся с библиотекой tRPC. Она позволяет достичь тех же результатов, но без необходимости использования кодогенерации.
Факты про tRPC
- Для формирования запросов на бэк часть используется React Query, поэтому по итогу на клиенте получается React Query-подобный синтаксис.
- Для работы tRPC требуется минимальная серверная часть. Серверной части, которая есть в SSR фреймворках типа Next.js вполне хватает, чтобы создать свой tRPC-сервер.
- На своем сайте заявляют, что tRPC используется в продакшене в тысячах компаний, так что уровень зрелости технологии вполне достаточный, чтобы брать ее на вооружение.
import { initTRPC } from "@trpc/server";
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
import superjson from "superjson";
import { ZodError } from "zod";
/ **
* 1. CONTEXT
*/
type CreateContextOptions = Record<string, never>;
const createInnerTRPCContext = (_opts: CreateContextOptions) => {
return {};
export const createTRPCContext = (_opts: CreateNextContextOptions) => {
return createInnerTRPCContext({});
/ **
* 2. INITIALIZATION
*/
export const t = initTRPC.context<typeof createTRPCContext>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
... shape,
data: {
... shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
}):
/ **
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
*/
export const createTRPCRouter = t. router;
export const publicProcedure = t.procedure;
import { exampleRouter } from "~/server/api/routers/example";
import { createTRPCRouter } from "~/server/api/trpc";
import { calculateRouter } from "./routers/calculate";
export const appRouter = createTRPCRouter({
example: exampleRouter,
calculate: calculateRouter,
});
// export type definition of API
export type AppRouter = typeof appRouter;
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
import { products, processors } from "~/server/types";
import calculate from "~/utils/calculate";
export const calculateRouter = createTRPCRouter({
calculate: publicProcedure
. input (z.object({
products,
processors,
}))
.query(({ input: { products, processors }}) => {
return calculate(products, processors);
}),
});
});
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
import { products, processors } from "~/server/types";
import calculate from "~/utils/calculate";
export const calculateRouter = createTRPCRouter({
calculate: publicProcedure
. input (z. object ({
products,
processors,
}))
.query(({ input: { products, processors }}) => {
return calculate(products, processors);
}),
Что еще почитать по теме?
- Официальный сайт trpc.io
- Генератор шаблонных проектов, который использует trpc там и ссылки на пояснительные видосы есть
- YouTube-канал Theo - t3․gg