Всем привет, меня зовут Сергей Прощаев, и в этой статье расскажу про то, как мы в современных проектах проектируем API, переходя от абстрактных идей к работающему коду. Тема эта не нова, но почему‑то до сих пор вокруг неё ломают копья. Видел проекты, где сначала писали код, а потом мучительно набрасывали документацию в Confluence, которая устаревала быстрее, чем появлялась. Видел команды, которые месяцами спорили о том, должен ли метод называться getCart или fetchCart. И видел, как после очередного релиза ловили баги, которые можно было предусмотреть на этапе спецификации.

Сегодня я хочу поделиться подходом, который помогает мне и моим командам избегать этой боли. Речь пойдет о Design‑First подходе с использованием OpenAPI Specification (OAS). Мы разберем не только теорию, но и практику: как описать API так, чтобы оно было понятно и бизнесу, и разработчику; как сгенерировать из этого описания готовый каркас приложения на Spring; и как сразу заложить в него механизмы глобальной обработки ошибок.

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

Давайте начнем.

Почему «сначала код» — это дорого?

Многие разработчики, включая меня на заре карьеры, любят писать код. Это кайф. Ты запускаешь IDE, создаешь контроллер, набрасываешь методы, и вот оно — API начинает дышать. Но через пару спринтов приходит владелец продукта и говорит: «А давайте мы еще поле middleName добавим». Вы его добавляете. Потом приходит фронтенд‑разработчик и спрашивает: «А это поле должно быть обязательным? В документации ничего не сказано». Вы лезете в код, ищете аннотации, вспоминаете логику двухнедельной давности.

Чем дольше проект живет в таком режиме, тем сильнее расходится реальность с документацией (или, что хуже, документация становится просто набором устаревших комментариев).

Был свидетелем проекта, где команда из 10 бэкенд‑разработчиков писала API для внутреннего портала. Использовался «код‑фирст» подход. Через полгода у команды был работающий код, но описание эндпоинтов существовало только в головах двух сеньоров, которые этот код проектировали. Когда один из них ушел в отпуск, а к интеграции подключилась новая команда, работа встала. Ушло много времени на то, чтобы понять, что именно ожидает сервер в теле запроса. Пришлось переписывать спецификацию, и тратить на это бесценное время, и в итоге все равно пришлось рефакторить контроллеры, потому что они не соответствовали тому, что в итоге все хотели видеть.

Вывод: Код сам по себе не является спецификацией. Он — реализация. Спецификация — это контракт. И если вы не описываете контракт первым, вы рискуете создать систему, где каждый эндпоинт живет по своим правилам.

OpenAPI Specification: ваш главный инструмент

Все бэкенд разработчики сталкиваются с OpenAPI Specification (ранее это был Swagger). Сейчас это стандарт де‑факто для описания REST API. И это не просто «какой‑то там YAML файлик». Это структурированный язык, который позволяет вам описать всё: от метаданных и версионирования до конкретных моделей данных и HTTP‑кодов ответов.

Основная идея: вы пишете описание API на YAML или JSON, а затем используете инструменты для генерации документации, кода сервера (Spring), клиентских SDK и даже тестов.

Структура OAS: что нужно знать

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

  1. openapi и info: Здесь вы указываете версию спецификации (например, 3.0.3) и метаданные API: название, версию, описание, контакты и лицензию. Это важно для документации.

  2. servers: Список URL‑адресов, где хостится API. Полезно для разных окружений (dev, prod, staging).

  3. paths: Самое сердце. Здесь вы описываете каждый эндпоинт и HTTP‑метод. Для каждого метода вы указываете параметры (path, query), тело запроса (requestBody) и возможные ответы (responses).

  4. components/schemas: Секция, где вы описываете модели данных. Это те самые объекты, которые передаются в теле запроса или ответа (например, ItemCart). Один раз описали — переиспользуете везде.

Лучшая практика: проектируйте модель данных первой

Например начинайте с описания моделей данных в components/schemas. Это позволит заранее договориться с фронтендом о структуре объектов. Если у нас был UserOrderPayment. Мы описали их типы, обязательные поля, форматы (например, date-time). И только когда модели будут утверждены, то можно переходить к описанию путей и методов.

Это избавляет нас от классической проблемы: «А почему в ответе приходит поле createdAt в одном формате, а в другом эндпоинте — в другом?». Модели становятся единым источником истины.

От спецификации к коду: автоматизация на Spring

Хорошо, спецификацию мы написали. Что дальше? Ручное написание всех контроллеров, которые реализуют эту спецификацию, отнимает время и чревато ошибками. Всегда был сторонником автоматизации, поэтому использую OpenAPI Generator. Это инструмент, который читает ваш YAML‑файл и генерирует:

  • Модели (POJO с аннотациями Jackson для JSON/XML).

  • Интерфейсы API (с аннотациями @RequestMapping@PathVariable@RequestBody).

Вот как это настроено у нас в проекте на Gradle.

1. Добавляем плагин в build.gradle:

plugins {
    id 'org.hidetake.swagger.generator' version '2.19.2'
}

В примере используется плагин org.hidetake.swagger.generator версии 2.19.2 — он рабочий и полностью покрывает потребности статьи. Однако для новых проектов рекомендуется официальный плагин OpenAPI Generator (org.openapi.generator версии 7.x), который активно поддерживается сообществом OpenAPITools. Синтаксис настройки немного отличается, но общая концепция (генерация кода по спецификации) остаётся той же.

2. Создаем конфигурацию для кодогенерации в файле src/main/resources/api/config.json. Здесь я указываю, что хочу использовать Spring MVC, Java 8 для работы с датами, пакеты для моделей и API, а также включаю поддержку HATEOAS и XML.

{
  "library": "spring-mvc",
  "dateLibrary": "java8",
  "modelPackage": "ru.otus.api.model",
  "apiPackage": "ru.otus.api",
  "useTags": true,
  "hateoas": true,
  "withXml": true
}

3. Настраиваем задачу генерации в build.gradle. Указываем путь к нашему openapi.yaml, конфигурационному файлу и список того, что хотим сгенерировать (модели и API). Также добавляем ApiUtil.java, который необходим для корректной работы сгенерированных интерфейсов.

swaggerSources {
    eStore {
        inputFile = file("${rootDir}/src/main/resources/api/openapi.yaml")
        code {
            language = 'spring'
            configFile = file("${rootDir}/src/main/resources/api/config.json")
            components = [models: true, apis: true, supportingFiles: 'ApiUtil.java']
        }
    }
}

4. Подключаем сгенерированные файлы к sourceSets, чтобы IDE и компилятор их видели.

После этого одна команда 

./gradlew clean build 

генерирует весь каркас. В папке build/swagger-code-eStore появляются Java‑файлы.

Идеально!

Рис. 1. Процесс разработки API по подходу Design-First с использованием OpenAPI Generator
Рис. 1. Процесс разработки API по подходу Design‑First с использованием OpenAPI Generator

Реализация: мы пишем только бизнес‑логику

Теперь у нас есть интерфейс CartApi с методами:

  • addCartItemsByCustomerId

  • getCartByCustomerId

  • и так далее

полностью аннотированный в соответствии со спецификацией. Нам остается только создать контроллер, который этот интерфейс реализует, и написать внутри бизнес‑логику.

@RestController
public class CartsController implements CartApi {
    private static final Logger log = LoggerFactory.getLogger(CartsController.class);

    @Override
    public ResponseEntity<List<Item>> addCartItemsByCustomerId(String customerId, @Valid Item item) {
        log.info("Request for customer ID: {}\nItem: {}", customerId, item);
        // Здесь будет вызов сервиса и бизнес-логика
        return ResponseEntity.ok(Collections.emptyList());
    }

    @Override
    public ResponseEntity<List<Cart>> getCartByCustomerId(String customerId) {
        // Здесь может быть выброшено исключение, если корзина не найдена
        throw new RuntimeException("Cart not found for customer: " + customerId);
    }
}

Обратите внимание: нам не нужно расставлять @RequestMapping@PathVariable. Всё это уже есть в сгенерированном интерфейсе. Мы просто фокусируемся на бизнес‑логике. Это и есть главная ценность подхода.

Обработка ошибок: не даем сбоям сломать опыт

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

Spring предлагает элегантное решение — Global Exception Handler через аннотацию @ControllerAdvice. Это мой любимый механизм. Создаем один класс, который будет перехватывать исключения из всех контроллеров.

Рис. 2. Последовательность обработки ошибок в Spring Framework
Рис. 2. Последовательность обработки ошибок в Spring Framework

Вот как это выглядит в коде c использованием @ExceptionHandler:

@ControllerAdvice
public class RestApiErrorHandler {
    private static final Logger log = LoggerFactory.getLogger(RestApiErrorHandler.class);

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<Error> handleRuntimeException(RuntimeException ex, HttpServletRequest request) {
        log.error("Runtime exception occurred: {}", ex.getMessage(), ex);
        Error error = ErrorUtils.createError(
            ErrorCode.GENERIC_ERROR.getErrMsgKey(),
            ErrorCode.GENERIC_ERROR.getErrCode(),
            HttpStatus.INTERNAL_SERVER_ERROR.value()
        );
        error.setUrl(request.getRequestURL().toString());
        error.setReqMethod(request.getMethod());
        return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
    }

    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    public ResponseEntity<Error> handleHttpMediaTypeNotSupported(HttpMediaTypeNotSupportedException ex, 
                                                                  HttpServletRequest request) {
        Error error = ErrorUtils.createError(
            ErrorCode.HTTP_MEDIATYPE_NOT_SUPPORTED.getErrMsgKey(),
            ErrorCode.HTTP_MEDIATYPE_NOT_SUPPORTED.getErrCode(),
            HttpStatus.UNSUPPORTED_MEDIA_TYPE.value()
        );
        error.setUrl(request.getRequestURL().toString());
        error.setReqMethod(request.getMethod());
        return new ResponseEntity<>(error, HttpStatus.UNSUPPORTED_MEDIA_TYPE);
    }
}

Здесь я определил Error — единый формат ответа об ошибке с кодом (внутренним, не HTTP), сообщением, статусом, URL и методом запроса. В ErrorCode — перечисление всех возможных ошибок с их кодами и сообщениями.

Теперь, если в любом контроллере возникнет RuntimeException, он будет обработан здесь, и клиент получит структурированный ответ с HTTP‑статусом 500 и нашим кастомным кодом. А если кто‑то пришлет запрос с Content-Type: application/octet-stream, получит 415 с понятным сообщением.

Нефункциональные требования: о чем нельзя забывать

Сильный проект отличается от слабого не только реализацией функционала, но и тем, как он справляется с нагрузкой, атаками и изменениями. Это Non‑Functional Requirements (NFR).

Для нашего USER‑сервиса я бы обязательно включил в спецификацию и архитектуру следующие моменты:

  • Производительность: Сервис должен выдерживать 500 RPS на чтение и 100 RPS на запись в час пик. Это требование сразу подсказывает: нужно кэширование (Redis) и асинхронная обработка для тяжелых операций.

  • Безопасность: Пароли только в хэшированном виде (bcrypt). Все эндпоинты, кроме /register и /login, защищены JWT. И это должно быть отражено в спецификации: либо через security схемы, либо через описание в документации.

  • Аудитинг: Каждое действие администратора должно логироваться с указанием кто, когда, что сделал. Это не функциональная фича, а требование к надежности и соответствию стандартам (например, GDPR). Мы можем добавить это в components как отдельную схему AuditLog или просто описать в требованиях, но кодогенерация тут не поможет — это задача для аспектно‑ориентированного программирования (AOP) в Spring.

Заключение: от инструмента к культуре проектирования

Мы прошли путь от проблемы хаотичной разработки API до системного подхода с использованием OpenAPI Specification и Spring. Мы научились:

  1. Описывать API контрактом первым, а не кодом.

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

  3. Централизованно обрабатывать ошибки, делая API предсказуемым и понятным для клиентов.

  4. Учитывать не только «счастливый путь», но и edge‑case сценарии, о чем нам напомнила история TSB.

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

Если вы хотите глубже погрузиться в тему проектирования API, научиться выстраивать процессы, где спецификация становится центральным звеном, и прокачать свои навыки — то можно пройти обучение на курсе «Проектирование API» в OTUS.

А чтобы узнать больше о формате обучения и задать свои вопросы, приходите на бесплатные уроки:

  • 1 апреля в 20:00. «Создание интерфейсов с помощью OpenAPI». Записаться

  • 15 апреля в 18:00. «Основы протокола HTTP». Записаться