Сервис развивается: тестируем формат, собираем идеи, улучшаем сервис. Есть идеи?

Написать
Войти
Дайджесты
Сравнение Point0 и tRPC: новые подходы к фулстек-разработке на React и Bun

Сравнение Point0 и tRPC: новые подходы к фулстек-разработке на React и Bun

Анализируем фулстек-фреймворк Point0 и сопоставляем его с библиотекой tRPC. Разбираем, как компилятор Point0 разделяет серверный и клиентский код в одном файле, как устроена автоматическая гидрация данных при SSR, поддержка z.file() в типизированных мутациях и рендеринг серверных компонентов в качестве ответов.

Сравнение Point0 и tRPC: новые подходы к фулстек-разработке на React и Bun

В экосистеме TypeScript обеспечение сквозной типизации между сервером и клиентом (End-to-End Type Safety) стало индустриальным стандартом. Разработчики больше не хотят вручную синхронизировать типы API, писать swagger-схемы или генерировать клиенты из OpenAPI. Библиотека tRPC долгое время оставалась главным инструментом для решения этой задачи в SPA-приложениях, предоставляя разработчикам иллюзию вызова серверных функций прямо на клиенте с полной проверкой типов на этапе компиляции.

Однако с развитием мета-фреймворков и среды выполнения Bun на рынке стали появляться полноценные фулстек-решения нового поколения. Одно из них — фреймворк Point0. В отличие от tRPC, являющегося лишь библиотекой для обвязки роутинга, Point0 представляет собой комплексный фреймворк на базе Bun и React. Разберем ключевые архитектурные различия между tRPC и Point0, сопоставим их код и оценим удобство разработки (Developer Experience, DX).

1. Объявление эндпоинтов и проблема масштабирования типов

Главное отличие лежит в способе организации роутов. В tRPC вы обязаны объявить каждую процедуру в локальном роутере, а затем импортировать все роутеры в единый корневой appRouter:

// tRPC: описание роута и ручная сборка
export const ideaRouter = router({
  getById: publicProcedure
    .input(z.object({ id: z.string() }))
    .query(async ({ input }) => {
      return await prisma.idea.findUniqueOrThrow({ where: { id: input.id } });
    }),
});

// Сборка в корневом файле сервера
export const appRouter = router({
  idea: ideaRouter,
  // При добавлении новых модулей список растет бесконечно
});
export type AppRouter = typeof appRouter;

Проблема масштабирования: По мере роста проекта тип AppRouter раздувается. Когда в системе появляется более 100 эндпоинтов, IDE (в частности, плагин TypeScript в VS Code) начинает существенно тормозить. Автодополнение кода может зависать на несколько секунд при вводе trpc.idea., так как компилятору приходится вычислять гигантские вложенные типы всего дерева роутов.

В Point0 концепция центрального роутера типов отсутствует. Каждая квери или мутация является самостоятельным импортируемым объектом:

// Point0: изолированное объявление
import { root } from './root';

export const ideaViewQuery = root.lets
  .query("ideaView")
  .input(z.object({ id: z.string() }))
  .handler(async ({ input }) => {
    return await prisma.idea.findUniqueOrThrow({ where: { id: input.id } });
  });

На клиенте вы импортируете конкретный запрос напрямую: import { ideaViewQuery } from './queries'. Типы этого запроса изолированы и не влияют на производительность IDE в других частях приложения. Индексация для рантайма собирается автоматически фоновым генератором фреймворка в файле конфигурации движка:

export const engine = Engine.create({
  points: [ideaViewQuery, ideaUpdateMutation],
});

2. Разделение клиентского и серверного кода: конвенции против компилятора

В tRPC безопасность исходного кода обеспечивается строгой дисциплиной каталогов. Серверный код (подключение к БД, секреты) должен физически лежать в серверных папках. Клиент импортирует только типы (import type { AppRouter }), что гарантирует отсутствие серверного кода в финальном JS-бандле браузера. Если разработчик случайно импортирует саму функцию роутера без ключевого слова type, серверные библиотеки попадут на клиент, вызвав ошибку сборки.

В Point0 за разделение отвечает умный компилятор (плагин для Bun/Vite/Babel). Вы можете писать серверный обработчик прямо в файле компонента или в общем файле API. При сборке клиентского бандла компилятор автоматически вырезает тело метода .handler(...) и все его серверные зависимости (например, импорты Prisma или AWS SDK), оставляя на клиенте только схему валидации инпута и типизированную обертку для сетевого запроса. Для сервера компилятор выполняет обратную операцию — вырезает клиентские React-хуки вроде useQuery.

3. Работа с файлами и FormData из коробки

Загрузка файлов в tRPC всегда была нетривиальной задачей. Поскольку tRPC по умолчанию сериализует данные в JSON, передача бинарных файлов требует либо ручной настройки обходных эндпоинтов, либо перекодирования файлов в Base64 (что увеличивает размер трафика на 33%), либо сложной настройки плагинов для работы с FormData.

Point0 решает эту проблему на уровне ядра фреймворка благодаря интеграции с Bun. Схема валидации Zod поддерживает валидацию файлов через z.file(). При передаче файла в мутацию фреймворк автоматически преобразует объект запроса на клиенте в FormData, отправляет его в бинарном виде, а на сервере парсит обратно в типизированный объект:

// Point0: Мутация с загрузкой файла
export const uploadAvatarMutation = root.lets
  .mutation("uploadAvatar")
  .input(z.object({
    userId: z.string(),
    avatar: z.file() // Нативная валидация файла
  }))
  .handler(async ({ input }) => {
    const bytes = await input.avatar.arrayBuffer();
    // Сохранение файла на сервере...
  });

4. Конфигурация бесконечной прокрутки (Infinite Queries)

В tRPC при реализации бесконечной прокрутки (useInfiniteQuery) наложено жесткое ограничение: поле курсора в объекте входных данных обязано называться строго cursor. Если вы хотите назвать его page или offset, библиотека выдаст ошибку типов.

Point0 предоставляет гибкость, позволяя указать любое поле в качестве курсора с помощью параметра pageParamFromInput:

export const ideaListQuery = root.lets
  .query("ideaList")
  .input(z.object({ limit: z.number(), page: z.number() }))
  .handler(async ({ input }) => {
    // Получение страницы данных...
  })
  .infinite({
    pageParamFromInput: "page", // Указываем кастомное имя поля
  });

5. Возврат серверных компонентов (RSC) и островов гидратации

tRPC предназначен исключительно для передачи данных (JSON). Он не знает о структуре разметки вашего приложения.

Поскольку Point0 является полноценным React-фреймворком, он поддерживает рендеринг серверных компонентов (React Server Components, RSC) прямо в ответах API. Вы можете вызвать мутацию, которая выполнит логику на сервере и вернет готовый отрендеренный HTML-кусок серверного компонента или ленивый интерактивный остров гидратации:

// Возврат компонента в ответе мутации
.handler(async ({ input }) => {
  return {
    html: <ServerSidebar activeId={input.id} />
  };
})

Сравнительная таблица: tRPC против Point0

КритерийtRPCPoint0
Тип решенияБиблиотека маршрутизации APIФулстек-фреймворк на базе Bun
Регистрация эндпоинтовРучная сборка в единое деревоАвтоматическая генератором проекта
Влияние на IDE (VS Code)Замедляет при росте числа роутовОтсутствует (импорты изолированы)
Разделение бандлаДисциплина импорта типов (import type)Автоматически компилятором
Загрузка файловТребует обходных путей или Base64Нативная поддержка через z.file()
Параметры пагинацииЖестко зафиксирован ключ cursorНастраиваемый pageParamFromInput
Поддержка RSC / SSRТолько данные (требует обвязки)Нативная интеграция, рендеринг островов

Заключение

tRPC остается надежным выбором для существующих проектов, где серверная часть уже написана на Node.js (Express, Fastify) или Next.js, и вам нужна только легкая сквозная типизация.

Однако если вы строите новое приложение с нуля, ориентируясь на современный стек (React 19+, Bun, SSR/RSC) и цените скорость работы IDE, Point0 предлагает более чистую архитектуру. За счет делегирования задач разделения кода компилятору и нативной поддержки бинарных данных, Point0 делает фулстек-разработку на TypeScript более монолитной и простой.