Прощай, документация API: сквозная типобезопасность приложений с tRPC

3 мин

1. Проблема «контракта» между фронтендом и бэкендом

При традиционной раздельной разработке фронтенда и бэкенда API служит «контрактом» между ними. Обычно этот контракт поддерживают следующими способами:

  • RESTful API: с помощью таких инструментов, как OpenAPI (Swagger), которые создают и поддерживают подробную документацию API.
  • GraphQL: с помощью строгого Schema Definition Language (SDL), определяющего структуры данных и операции.

Эти подходы работают, но у них есть общая проблема: контракт отделён от реализации. Вызывая API, фронтенд-разработчик верит, что тот вернёт структуру данных, описанную в документации или схеме. Если реализация бэкенда изменилась — например, было переименовано поле, — а документация или схема не обновились вовремя, несоответствие проявится только во время выполнения и приведёт к ошибке.

Можно ли автоматически синхронизировать этот «контракт» с реализацией и даже выявлять несоответствия во время компиляции?

2. Главная идея tRPC: общие типы вместо схем

tRPC (TypeScript Remote Procedure Call) предлагает радикальное, но крайне простое решение: если и фронтенд, и бэкенд написаны на TypeScript, почему бы не использовать типы напрямую?

tRPC позволяет писать обычные функции TypeScript в качестве API бэкенда и непосредственно вызывать их во фронтенде с полной поддержкой вывода типов и автодополнения — словно это функции локального модуля.

Ни схемы, ни генерация кода не требуются. Единственный «контракт» — сами типы TypeScript.

3. Как это работает?

Магия tRPC основана на выводе типов и небольшой хитроумной обёртке.

a. Бэкенд: определяем API Router

На бэкенде — обычно в сервисе Node.js — с помощью функций tRPC создаётся один или несколько «Router». Каждый Router представляет собой набор доступных для вызова «Procedure», то есть конечных точек API.

// server/router.ts
import { initTRPC } from '@trpc/server';
import { z } from 'zod'; // 使用 Zod 进行运行时校验

const t = initTRPC.create();

export const appRouter = t.router({
  // 定义一个名为 `getUser` 的查询 procedure
  getUser: t.procedure
    .input(z.object({ userId: z.string() }))
    .query(({ input }) => {
      // 在这里查询数据库或执行其他逻辑
      const user = { id: input.userId, name: 'Alex' };
      return user;
    }),

  // 定义一个名为 `createUser` 的变更 procedure
  createUser: t.procedure
    .input(z.object({ name: z.string() }))
    .mutation(({ input }) => {
      const user = { id: `${Math.random()}`, name: input.name };
      return user;
    }),
});

// 导出 router 的类型定义
export type AppRouter = typeof appRouter;

b. Фронтенд: создаём клиент и вызываем API

Во фронтенде достаточно импортировать из бэкенда AppRouter как тип. Обратите внимание: импортируется только тип, а не серверный код.

// client/trpc.ts
import { createTRPCReact } from '@trpc/react-query';
import type { AppRouter } from '../server/router'; // 只导入类型

export const trpc = createTRPCReact<AppRouter>();

Теперь API можно вызывать из компонента React словно локальную функцию, пользуясь полной типобезопасностью и автодополнением.

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

function UserInfo({ userId }: { userId: string }) {
  // `useQuery` 的第一个参数是 procedure 的路径
  // 你输入 `trpc.` 时,IDE 会自动提示 `getUser` 和 `createUser`
  const userQuery = trpc.getUser.useQuery({ userId });

  if (userQuery.isLoading) {
    return <div>Loading...</div>;
  }

  // `userQuery.data` 的类型被自动推断为 { id: string; name: string }
  return <div>User: {userQuery.data?.name}</div>;
}

Если теперь разработчик бэкенда переименует поле, возвращаемое getUser, с name на fullName, выражение userQuery.data?.name во фронтенде немедленно вызовет ошибку компиляции TypeScript, а не обнаружится лишь во время выполнения.

4. Почему стоит выбрать tRPC?

  • Абсолютная сквозная типобезопасность: в этом заключается его основная ценность. Устраняется целый класс ошибок, вызванных несовпадением контрактов API.
  • Превосходный процесс разработки: благодаря автодополнению IDE больше не нужно сверяться с документацией или угадывать структуру API. Рефакторинг становится исключительно простым и безопасным.
  • Без генерации кода: дополнительных этапов сборки нет, поэтому обратная связь поступает очень быстро.
  • Лёгкость и гибкость: сам tRPC очень мал и может интегрироваться с любым фронтенд-фреймворком и бэкенд-сервисом.

Заключение

tRPC делает full-stack-разработку на TypeScript беспрецедентно плавной. Отказ от зависимости от документации API и схем обеспечивает бесшовное и крайне безопасное взаимодействие фронтенда с бэкендом. В архитектуре монорепозитория эти преимущества становятся ещё заметнее. Если вы создаёте full-stack-приложение на TypeScript, tRPC — революционный инструмент, которому определённо стоит уделить время.