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

Почему стоит обратить внимание на Tapir

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

Tapir не навязывает конкретный серверный стек — описание endpoint’ов остаётся независимым от Akka HTTP, http4s или ZIO-Http, что упростит миграции и тестирование. Для команд, которые стремятся к типобезопасности и готовятся к эволюции архитектуры, это серьёзное преимущество.

Ключевые концепции и базовый паттерн

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

Описание обычно строится цепочкой combinator-ов: метод, путь, параметры, тела и выходы. После этого endpoint «интерпретируется» в конкретный серверный обработчик или превращается в документацию OpenAPI/Swagger.

Endpoint как значение

Типичный endpoint записывается коротко и наглядно. Вы явно видите: какие параметры обязательны, какие опциональны, какой сериализатор используется для тела. Это облегчает чтение кода и делает контракты самодокументированными.

Например, endpoint для получения пользователя по id описывает путь, тип id и формат ответа — без смешивания маршрутизации и бизнес-логики. Такой код легче тестировать и рефакторить.

Inputs, outputs и error handling

Inputs покрывают путь, query, headers, куки и тело. Outputs — успешные ответы и ошибки, причём ошибки тоже имеют типы, что позволяет точно управлять статус-кодами и форматами ответов.

Важно: статусы и схемы ошибок явно указаны в описании. Это снижает риск рассинхронизации между документацией и поведением сервиса, особенно в крупных командах.

Сериализация и codec’и

Tapir интегрируется с популярными JSON-библиотеками — например, circe или play-json — через codec-пакеты. Вы регистрируете имплицитный сериализатор для своего типа и используете jsonBody[MyType] в описании endpoint’а.

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

Практический пример: от описания до сервера

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

import sttp.tapir._
import sttp.tapir.json.circe._
import io.circe.generic.auto._

case class User(id: Int, name: String)

val getUser = endpoint.get
  .in("users" / path[Int]("id"))
  .out(jsonBody[User])

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

После этого тот же endpoint используется для генерации OpenAPI-документа и отображения Swagger UI, что экономит время и устраняет несоответствия между кодом и документацией.

Интеграция с экосистемой: серверы, клиенты и документация

Tapir поддерживает множество серверных и клиентских интерпретаторов: http4s, Akka HTTP, ZIO-Http и другие. Это даёт гибкость при выборе runtime и при необходимости смены стека без переписывания описаний API.

Кроме серверов, Tapir умеет генерировать OpenAPI-спецификации и подключать Swagger UI или Redoc. Также из описаний можно генерировать клиентский код, что упрощает интеграцию между сервисами.

Генерация OpenAPI

Документы OpenAPI формируются автоматически из endpoint’ов: типы входов и выходов преобразуются в схемы, перечисления и форматы сериализации отражаются в спецификации. Это сокращает ручную работу по поддержке документации.

В сочетании со CI можно публиковать актуальные спецификации и даже валидировать изменения API до слияния пулл-реквеста.

Типичные паттерны использования и советы

Разделяйте описание endpoint’ов и бизнес-логику. Пусть endpoint остаётся декларацией контракта, а обработчик возвращает или Future/IO с результатом. Это упрощает тесты и позволяет переиспользовать описания для разных интерпретаторов.

Используйте явные типы ошибок. Мелкие команды часто экономят на типизации ошибок, но это приводит к смешению статусов и сообщений. Явные ADT для ошибок делают контракт понятным и облегчают интеграцию.

Версионирование API

Описания удобнее версионировать на уровне namespace-путей или через добавление версии в путь. Tapir не навязывает метод версионирования, зато делает его очевидным в коде: v1 и v2 — разные наборы endpoint’ов.

При этом полезно держать миграционные тесты, проверяющие поведение старых версий после изменений в логике сервисов.

Небольшая сводная таблица: где Tapir выигрывает

Критерий Tapir Традиционные маршрутизационные подходы
Типобезопасность Высокая — контракты как типы Низкая — маршруты и строки
Документация Генерация OpenAPI из описаний Часто ручная синхронизация
Зависимость от runtime Независимы — один контракт для многих бэкендов Плотная связь с фреймворком

Ошибки и подводные камни, которые я встречал

Самая частая проблема — путаница между моделями для внутренней логики и моделями для API. Я рекомендую держать DTO отдельно и явно конвертировать их в доменные объекты, чтобы избежать утечек реализации наружу.

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

Как начать: минимальный чек-лист

Если вы решаете попробовать Tapir в новом проекте, пройдите по простому плану: сначала опишите несколько ключевых endpoint’ов, затем свяжите их с простыми stub-обработчиками и запустите локальный сервер. Затем подключите генерацию OpenAPI и посмотрите, как обновляется документация.

  • Добавьте зависимость Tapir и выбранной JSON-библиотеки.
  • Опишите несколько endpoint’ов как значения.
  • Интерпретируйте их в выбранном сервере и напишите интеграционные тесты.
  • Подключите генерацию OpenAPI и Swagger UI.

Личный опыт

В одном из проектов я использовал Tapir, чтобы объединить несколько микросервисов с разными runtime’ами. Описания позволили автоматически генерировать клиентский код и сократить количество ошибок при интеграции. Команда оценил ясность контрактов — код стал читаться, как спецификация и реализация одновременно.

В другом случае Tapir упростил миграцию от http4s к ZIO-Http: достаточно было заменить интерпретатор, а описания остались прежними. Это сэкономило недели работы и снизило риск регрессий.

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