02 авг. 2024 г.·6 мин чтения

ts-rest vs tRPC vs OpenAPI codegen: как выбрать

ts-rest vs tRPC vs OpenAPI codegen: сравните строгость контракта, подходящесть для команды и объём связующего кода, чтобы выбрать API-подход, который сэкономит время.

ts-rest vs tRPC vs OpenAPI codegen: как выбрать

С какой проблемой сталкиваются команды

Команды обычно начинают с простого плана: выпустить бэкенд, подключить фронтенд и держать типы в синхроне. Это работает для первой версии. Потом выходит несколько быстрых релизов, один эндпоинт меняет форму, поле становится необязательным, а фронтенд всё ещё вызывает старый контракт.

Люди вручную закрывают этот разрыв. Один разработчик пишет маппер в клиенте. Другой копирует типы бэкенда в веб-приложение. Мобильное приложение добавляет свой адаптер, потому что не может переиспользовать тот же helper. Сначала всё это не выглядит серьёзно. Через несколько недель в кодовой базе уже лежат маленькие кучки связующего кода, разбросанные повсюду.

Цена — это не только баги. Это ещё и время, потраченное на скучные проверки и повторяющиеся исправления. Кто-то тратит 20 минут, чтобы понять, "status" — это строка, enum или две разные сущности в двух репозиториях. Обработка ошибок тоже усложняется. Сервер возвращает один формат, клиент ждёт другой, и никто не хочет трогать старый адаптер, который "в основном работает".

Небольшая команда какое-то время может это игнорировать. Представьте стартап с одним бэкенд-инженером и двумя фронтенд-инженерами, которые выпускают обновления три раза в неделю. Они двигаются быстро, но и полезную нагрузку меняют так же быстро. Без общего контракта они после каждого релиза тратят больше времени на исправление поломок, чем на новые функции.

Командам также нужны разные уровни строгости API-контракта. Публичному API важны стабильность, версии и понятная документация, потому что на него опираются внешние пользователи. Внутреннему приложению чаще нужна скорость, а не церемония, особенно если одна и та же команда владеет обеими сторонами и обновляет их вместе.

Именно поэтому выбор между ts-rest, tRPC и OpenAPI codegen на самом деле сводится к владению и скорости изменений. Если одна команда контролирует фронтенд и бэкенд, более строгий контракт помогает сэкономить на переделках. Если от API зависят несколько команд, внешние клиенты или долгоживущие интеграции, более жёсткие границы обычно экономят больше боли потом.

Что вы на самом деле выбираете

Этот выбор — не столько про синтаксис, сколько про то, как работает ваша команда. Вы решаете, насколько тесно будут двигаться фронтенд и бэкенд, насколько болезненным окажется breaking change и сколько инструментов вы готовы поддерживать.

Если обе стороны ведёт одна команда, тесная связка часто ощущается нормально. Те же люди меняют маршрут, обновляют клиент, запускают тесты и выпускают всё за один проход. В такой схеме совместное использование строгих типов убирает много связующего кода, потому что никто не ждёт другую команду.

Но всё меняется, когда у API появляется несколько потребителей. Веб-приложение, мобильное приложение, админка и партнёрская интеграция не обновляются в один и тот же день. Как только несколько клиентов завязаны на один контракт, небольшие изменения на бэкенде перестают быть небольшими. Переименование поля превращается в поддержку, версионирование и дополнительные шаги релиза.

Breaking changes — это место, где команды чаще всего неверно оценивают компромисс. Если вы можете быстро их переваривать, более тесный контракт жить не мешает. Если нет, нужен контракт, который явно описан, стабилен и легко делится за пределами одной кодовой базы.

Стоимость инструментов тоже важна. Одни команды спокойно поддерживают генераторы, схемы, общие пакеты и шаги сборки. Большинство — нет. Будьте честны с собой. Более красивый контракт на бумаге не лучше, если через шесть месяцев его никто не хочет поддерживать.

Хорошо работает простое правило:

  • одна команда, один основной клиент, быстрые релизы: обычно подходят более тесные контракты;
  • несколько клиентов, более медленные релизы, внешние потребители: явно описанные контракты лучше выдерживают время;
  • мало терпимости к инструментам: выбирайте путь с меньшим числом движущихся частей.

Именно поэтому разные команды выбирают разные инструменты и сначала все кажутся правильными. Сложность в том, чтобы сопоставить строгость контракта с тем, как вы выпускаете продукт, сколько клиентов поддерживаете и сколько сопровождения реально готовы терпеть.

Когда tRPC подходит лучше всего

tRPC лучше всего подходит, когда одна TypeScript-команда владеет и фронтендом, и бэкендом. Те же люди определяют процедуры, входные данные и типы ответа, а потом используют их с обеих сторон без отдельного ручного контракта. Это сильно снижает трение, когда продукт меняется каждую неделю.

Главное преимущество — близость. Серверная логика и клиентские типы остаются рядом, поэтому переименованное поле или изменённый входной параметр обычно ломается на этапе компиляции, а не превращается в баг на позднем QA. Для внутренних дашбордов, админок и ранних SaaS-функций такую скорость сложно игнорировать.

Самый понятный пример — небольшой стартап. Допустим, два инженера строят React-приложение и Node-бэкенд. Они часто выпускают изменения, постоянно меняют модель данных и пока не нуждаются в публичном API. В такой конфигурации tRPC ощущается простым. Он помогает сохранять темп и убирает лишние схемы, генераторы и обёртки только ради передачи JSON.

tRPC хорошо работает и на раннем этапе продукта, когда API ещё только устаканивается и команде нужен быстрый фидбек. Для стартапов это обычная история. Идеи меняются каждую неделю. Если одни и те же люди трогают обе стороны стека, прямой TypeScript-first подход обычно ощущается легче.

Неловкость появляется, когда API перестаёт быть только для TypeScript. Если тот же контракт нужен Swift-мобильному приложению, Kotlin-сервису, Python-воркеру или партнёрской интеграции, tRPC уже менее удобен. Его сила — в том, что TypeScript говорит с TypeScript.

Если ваш стек в основном TS и API нужен только внутри, tRPC часто — самый чистый способ получить типобезопасные API-контракты без лишнего инструментария.

Когда ts-rest подходит лучше всего

ts-rest — это золотая середина, которую многие TypeScript-команды и правда хотят. Он оставляет API обычным HTTP, с явными маршрутами, методами, параметрами и кодами статуса, но не заставляет уходить в тяжёлый spec-first-процесс.

Это важно, когда команде нравится REST и она хочет, чтобы контракт оставался видимым. Фронтенд-разработчик всё ещё видит, что endpoint — это GET /projects/:id с определённой формой ответа. Ничего не спрятано за вызовами функций, и для синхронизации клиента и сервера не нужно много самодельной обвязки.

ts-rest хорошо работает, когда одним и тем же бэкендом пользуются несколько клиентов. Веб-приложение, админка и мобильное приложение могут использовать одинаковые формы эндпоинтов без догадок о том, как должны выглядеть запросы. Для такого сценария ts-rest часто подходит лучше, чем tRPC: API ведёт себя как API, а не как внутренний TypeScript-поток.

Типичный пример стартапа выглядит так: приложение на Next.js для клиентов, мобильное приложение для сотрудников и, возможно, небольшой инструмент для админов. Всем им нужны одни и те же endpoints для заказов, авторизации и отчётности. С ts-rest команда хранит один контракт на TypeScript и переиспользует его с обеих сторон, при этом всё ещё говорит на обычном HTTP.

Компромисс простой. ts-rest требует больше проектирования на старте, чем tRPC. Нужно заранее решить названия маршрутов, формы запросов, коды статуса и ответы на ошибки, прежде чем вы начнёте двигаться быстро. Первую неделю это может раздражать. Но многие потом радуются, что сделали это один раз, когда появляется второй или третий клиент.

Если вам нужны понятные контракты поверх HTTP без перехода к полноценной генерации из OpenAPI, ts-rest часто оказывается самым практичным выбором.

Когда OpenAPI codegen подходит лучше всего

Подгоните контракт под рабочий процесс
Настройте контракты API, CI и документацию так, как ваша команда действительно работает и выпускает изменения.

Если ваш API выйдет за пределы одной команды, OpenAPI codegen обычно безопаснее. Он лучше всего подходит, когда от одного и того же бэкенда зависят несколько приложений, команд или внешних компаний. Письменная спецификация даёт всем один источник правды, даже если они никогда не открывают код сервера.

Это особенно важно для публичных API и продуктов с разными типами клиентов. React-приложение может быть лишь одним потребителем. Добавьте iPhone-приложение, Android-приложение, админку и партнёрскую интеграцию — и слабые соглашения начинают ломаться. OpenAPI даёт самый сильный внешний контракт из трёх, потому что контракт существует сам по себе, вне вашего TypeScript-кода.

Обычно он хорошо подходит, когда:

  • у вас есть веб-, мобильные и партнёрские клиенты, использующие одни и те же endpoints;
  • часть потребителей написана не на TypeScript;
  • разные команды выпускают изменения по разным графикам;
  • обратная совместимость важнее, чем скорость в рамках одного спринта.

В такой схеме сгенерированные клиенты и модели уменьшают число мелких постоянных ошибок. Люди перестают спорить о названиях полей, nullable-значениях, формах ошибок и правилах пагинации.

Но цена реальна. Кому-то нужно поддерживать спецификацию, внимательно проверять изменения и запускать генерацию как часть сборки или CI. Сгенерированный код тоже может казаться неудобным, поэтому команды всё равно пишут поверх него обёртки. OpenAPI codegen не убирает весь связующий код. Он переносит больше работы в проектирование и проверку контракта.

Такой обмен часто оправдан, когда важнее стабильность, а не скорость. Если вы строите публичный API, поддерживаете несколько клиентских платформ или планируете долгоживущие интеграции, дополнительный процесс — это страховка.

Как выбрать, не перегружая себя

Большинство команд усложняют этот выбор сильнее, чем нужно. Они сначала влюбляются в инструмент, а потом пытаются подогнать под него все решения по API. Обычно это создаёт больше работы позже.

Лучше посмотреть, кто будет вызывать API и как часто эти потребители меняются. Это и подсказывает, насколько строгий контракт вам действительно нужен.

Сначала перечислите всех клиентов, которые трогают API: основное веб-приложение, мобильные приложения, админки, фоновые задачи, внутренние скрипты и внешние интеграции. Потом отметьте, какие из них меняются часто, а какие должны оставаться стабильными месяцами. После этого выбирайте самый лёгкий контракт, который всё ещё защищает стабильных клиентов от поломок.

Для проверки не нужен большой план миграции. Возьмите одну среднюю по размеру функцию. Настройки аккаунта, биллинг, поиск или админский сценарий вполне подойдут. Такие задачи обычно достаточно "грязные", чтобы показать реальное трение.

У маленького стартапа компромиссы особенно хорошо видны. Допустим, у вас есть одно React-приложение для клиентов, одна внутренняя админка и одно мобильное приложение. Два инженера вместе ведут фронтенд и бэкенд, поэтому каждый лишний слой стоит времени.

Если большая часть продуктовой работы живёт в React и TypeScript, tRPC часто ощущается лучше всего. Один инженер меняет процедуру, другой обновляет интерфейс, и команда продолжает двигаться.

Если мобильное приложение уже важно или вы скоро ждёте смешанных клиентов, ts-rest обычно даёт более безопасную середину. API остаётся понятным и стандартным, а переход к большему числу клиентов проходит менее болезненно.

Если от бэкенда уже зависят отдельные команды, партнёры или пользователи публичного API, не тяните — формализуйте контракт через OpenAPI.

Не стандартизируйте всё после игрушечного демо. Сделайте что-то реальное. Если для этой задачи нужны обёртки, дублирование типов или ручные исправления, чтобы фронтенд и бэкенд оставались согласованы, значит, первый выбор, скорее всего, слишком слабый или слишком жёсткий.

Лучший инструмент обычно тот, который защищает то, что должно быть стабильным, и не мешает тому, что должно быстро меняться.

Где всё равно появляется связующий код

Проверьте ваш стек на практике
Разберите одну реальную задачу и посмотрите, где настройка контрактов создаёт лишнюю работу.

Даже при общих контрактах некоторые стыки всё равно остаются ручными. Эти инструменты сильно сокращают лишнюю работу, но не решают за вас, как приложение будет работать с сессиями, крайними случаями или деталями транспорта.

Auth — самый частый пример. Клиент всё равно должен прикреплять заголовки, определять истёкший токен, обновлять его и повторять запрос один раз. Контракт может подсказать, какому endpoint нужен auth. Но он не решит, где живёт логика обновления токена и как не допустить, чтобы три неудачных запроса одновременно пытались его обновить.

Небольшие расхождения в данных переживают любой подход. Бэкенд может отправлять дату в виде строки ISO, а UI для сортировки и отображения хочет объект Date. Enum кажется простым, пока одна сторона не добавляет "archived", а другая всё ещё ждёт только "active" или "disabled". null и undefined создают такие же проблемы. Контракт говорит, что поле необязательное, а ваша форма считает отсутствие и пустое значение двумя разными состояниями.

Обработка ошибок расходится быстрее, чем ожидает большинство команд. Один маршрут возвращает { code, message }, другой добавляет details, а третий шлёт обычный текст, потому что кто-то скопировал старый middleware. Если заранее задать один формат ошибок, фронтенд сможет показывать сообщения единообразно, а логи станут понятнее.

Загрузки и стриминг требуют отдельного внимания, потому что они ведут себя не как обычные JSON-запросы. Для загрузки файлов нужны правила по размеру, типу контента, прогрессу и сообщениям об ошибках. Для потоковых маршрутов нужно решить, как работать с переподключением, частичными данными и таймаутами.

Короткий чек-лист помогает:

  • задайте единый формат ошибок;
  • решите, как даты превращаются в значения для UI;
  • осознанно обработайте null и undefined;
  • один раз напишите правила повторных попыток для auth;
  • вручную протестируйте загрузки и потоки.

Команды, которые решают эти вещи заранее, потом пишут гораздо меньше связующего кода.

Частые ошибки на старте

Многие команды выбирают слишком строгую схему ещё до того, как API перестаёт меняться. Это кажется осторожным, но часто создаёт лишнюю возню. Когда endpoints, названия полей и форматы ошибок меняются каждые несколько дней, жёсткая схема делает простые продуктовые изменения слишком дорогими.

Ещё одна частая ошибка — смешивать подходы между сервисами без понятной причины. Одна команда использует tRPC для внутренних инструментов, ts-rest для одного приложения и OpenAPI для публичного сервиса, просто потому что каждому разработчику нравится что-то своё. В результате получается больше накладных расходов, а не меньше. Людям приходится помнить разные правила для ошибок, auth, валидации и генерации клиентов.

Сгенерированные клиенты тоже не спасают плохой API. Если у маршрутов непонятные названия, списковые endpoints возвращают разные формы или один сервис использует три формата ошибок, codegen просто повторит все шероховатости. Фронтенду всё равно понадобятся адаптеры, особые случаи и защитные проверки.

Команды также забывают, что контракт помогает только тогда, когда тесты и документация — часть обычной работы. У стартапа может генерироваться свежий клиент после каждого изменения на бэкенде, но если никто не запускает contract tests в CI и не обновляет примеры запросов, типизированный клиент создаёт ложное чувство безопасности. Код компилируется, но реальная интеграция всё равно ломается.

Лучше выработать скучную, но эффективную привычку: один подход для одной границы, одинаковое проектирование маршрутов и обновление документации и тестов в тот же день, что и контракт.

Быстрые проверки перед окончательным решением

Получите совет Fractional CTO
Используйте опыт Oleg в стартапах и роли Fractional CTO, чтобы закрыть вопросы по API-дизайну и поставке продукта.

Хороший API-инструмент должен пережить скучный вторник. Дайте новому коллеге небольшую задачу, например добавить маршрут, который читает запись и возвращает два поля. Если большую часть часа он тратит на типы, адаптеры и клиентские помощники, значит, схема для вашей команды слишком тяжёлая.

Вот несколько прямых вопросов:

  • Может ли кто-то добавить один маршрут, вызвать его из фронтенда и обработать одну ошибку меньше чем за час?
  • Если бэкенд-разработчик переименует поле, сломается ли фронтенд в локальной разработке или CI ещё до релиза?
  • Может ли второй клиент, например мобильное приложение или внутренний скрипт, использовать тот же контракт без своих обёрток?
  • Подходит ли инструмент той команде, которая у вас есть сейчас, с текущим стеком и привычками?

Последний вопрос важнее, чем многие готовы признать. Команды часто выбирают инструмент под будущую оргструктуру, которой ещё не существует. Маленькая TypeScript-команда может очень быстро двигаться с tRPC. Команде, которой нужны более понятные HTTP-контракты или больше одного типа клиента, обычно комфортнее с ts-rest или OpenAPI codegen.

Один простой эксперимент скажет вам очень многое. Возьмите один реальный endpoint и набросайте его тремя способами. Включите в схему валидацию запроса, auth context, один вызов с фронтенда и одно breaking change. Вы узнаете больше, чем из любой сравнительной таблицы функций.

Что делать дальше

Выберите одну функцию и проверьте свой выбор на реальной работе в течение недели или двух. Не начинайте с всего продукта. Лучше всего подходит небольшая, но не слишком чистая задача, например auth, биллинг или страница дашборда, которая затрагивает несколько endpoints.

Смотрите не только на то, работает ли демо, а на трение. Команды обычно узнают больше из одной раздражающей недели, чем из трёх архитектурных встреч.

Записывайте четыре вещи: как часто расходятся фронтенд и бэкенд, сколько ручного маппинга вы добавляете, сколько времени занимают простые изменения API от начала до конца и может ли новый участник команды разобраться без помощи.

Назначьте дату пересмотра до старта. Обычно хватает двух–четырёх недель. Ранние решения кажутся постоянными, хотя это не так. Если инструмент подходит сейчас, но начинает мешать, когда у приложения появляются новые пользователи, новые сервисы или внешние потребители, вам нужен чёткий момент, чтобы пересмотреть выбор.

Сделайте запасной выход простым. Храните схемы рядом с логикой маршрутов, избегайте излишне умных обёрток и держите генерацию клиентов скучной и предсказуемой. Это даст вам пространство перейти к более строгим контрактам позже без полной переписки проекта.

Если вам нужен второй взгляд, прежде чем стандартизировать один подход, Oleg Sotnikov на oleg.is проводит ревью API-дизайна, инфраструктуры и рабочего процесса команды в рамках своей работы Fractional CTO со стартапами и небольшими компаниями. Такой разбор особенно полезен до того, как один подход начнёт распространяться по всей кодовой базе.

Часто задаваемые вопросы

Как выбрать между tRPC, ts-rest и OpenAPI codegen?

Начните не с инструмента, а с того, кто будет вызывать API. Если и фронтенд, и бэкенд ведёт одна TypeScript-команда и релизы идут быстро, подойдёт tRPC. Если вам нужны понятные HTTP-маршруты и вы ожидаете больше одного клиента, берите ts-rest. Если от API зависят внешние команды, партнёрские приложения или публичные пользователи, выбирайте OpenAPI codegen.

Когда tRPC подходит лучше всего?

Используйте tRPC, когда одна TypeScript-команда контролирует обе стороны и продукт часто меняется. Он держит типы рядом с серверным кодом и ловит многие ошибки контракта ещё во время разработки. Это хорошо подходит для внутренних инструментов, админок и ранних SaaS-функций.

Почему мне выбрать ts-rest вместо tRPC?

Выбирайте ts-rest, когда хотите обычные REST-endpoints с общим TypeScript-контрактом. Он даёт видимые маршруты, методы, параметры и коды статуса без тяжёлого spec-first-подхода. Командам он часто нравится, когда один и тот же бэкенд используют веб-приложение, админка и мобильное приложение.

Когда стоит сразу идти в OpenAPI codegen?

Выбирайте OpenAPI codegen, когда API должно существовать само по себе. Обычно это публичные API, партнёрские интеграции, отдельные команды или клиенты не на TypeScript. На контракт вы потратите больше времени, но потом сэкономите на поломках и несовпадающем клиентском коде.

Что делать, если у меня уже есть и веб, и мобильный клиент?

Появление мобильного приложения сразу меняет баланс. У tRPC часть преимуществ исчезает, потому что его сильная сторона — TypeScript по обе стороны. Если мобильное приложение уже важно, ts-rest обычно даёт более аккуратный путь. Если одним и тем же API будут пользоваться сразу несколько мобильных и партнёрских клиентов, OpenAPI чаще подходит лучше.

Уберёт ли один из этих инструментов весь связующий код?

Нет. Они сильно сокращают повторяющуюся работу с типами, но не решают вообще все стыки в приложении. Вам всё равно нужно вручную продумать auth, разбор дат, формат ошибок, загрузки, потоки и несколько преобразований, завязанных на UI.

Где связующий код обычно всё ещё появляется?

Чаще всего первой проблемой становится auth-логика. Клиент всё равно должен прикреплять токены, обновлять их и не запускать три повторных запроса одновременно. Даты тоже часто создают проблемы, потому что сервер отправляет строки, а UI нужны настоящие объекты даты. Форматы ошибок, обработка null, загрузки и потоковые маршруты тоже требуют ясных правил.

Не слишком ли тяжёл OpenAPI для раннего стартапа?

Да, если ваш API всё ещё меняется каждые несколько дней и им пользуется только одна небольшая команда. На этом этапе тяжёлая схема и процесс генерации клиентов могут замедлять простую продуктовую работу. Лучше начать легче, проверить форму продукта, а потом ужесточать контракт, когда появятся новые клиенты или команды.

Стоит ли использовать разные инструменты для разных сервисов?

Обычно нет. Выбирайте один подход на одну границу API и придерживайтесь его, если только у вас нет очень веской причины делать иначе. Если каждый сервис использует свой стиль, команде приходится помнить разные правила для auth, ошибок, валидации и настройки клиентов, а это добавляет больше накладных расходов, чем экономит.

Как проверить свой выбор, прежде чем окончательно решить?

Возьмите одну реальную задачу на одну-две недели. Выберите что-то достаточно живое, чтобы проявилась трение: auth, биллинг или сценарий дашборда. Посмотрите, как часто расходятся фронтенд и бэкенд, сколько кода для преобразований вы пишете и сколько времени занимает небольшое изменение API от серверного кода до интерфейса. Это расскажет больше, чем любая сравнительная таблица.