Типизация REST API для фронтенд разработчика

Сегодня широкое распространение имеют следующие подходы для описания взаимодействия браузера и сервера, такие как OpenApi & GraphQL.

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



Эти инструменты помогают нам:

  • Разрабатывать и моделировать API в соответствии со стандартами на основе спецификаций
  • Создавать стабильный, многократно используемый код для вашего API практически на любом языке
  • Улучшить опыт разработчика с помощью интерактивной документации API
  • Легко проводить функциональные тесты на ваших API
  • Создавать и применять лучшие рекомендации по стилю API в вашей API архитектуре

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

Фронтенд приложение не скомпилируется если команда допустила ошибку в типах данных, которые принимает/поставляет REST API.

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

Для того чтобы получить все вышеописанные преимущества статически типизированного API нам необходимо воспользоваться генератором кода, который по OpenAPI спецификации сможет сгенерировать файлы с описанием типов, для Typescript это *.d.ts файлы.

В нашем проекте используется микросервисная архитектура и весь бекенд написан на .NET поэтому для генерации клиентов API для фронтенда/бэкенда мы использовали NSwag. Этот инструмент позволяет генерировать OpenAPI документы, которые затем в свою очередь уже используются для генерации кода клиентов.

Архитектура


В общем случае процесс генерации кода состоит из нескольких этапов:

  • Генерация OpenAPI документа
  • Генерация кода по OpenAPI документу

В нашем случае бекенд написан с использованием .Net и основной язык разработки C# этим обусловлен выбор инструментов. Мы для генерации OpenAPI документов и клиентов используем один и тот же инструмент под названием NSwag.


Рисунок 1 Архитектура решения по генерации кода rest клиентов

На рисунке обозначены:

  • Microservice 1...N — микросервисы предоставляющие API
  • NSwag — генерация OpenAPI документа из кода API микросервиса (микросервис может быть написан на любом ЯП для которого есть инструмент для генерации документа OpenAPI)
  • NSwag — генерация кода API клиента по OpenAPI документации (есть много инструментов можно выбрать тот, что больше подходит под ваш стек технологий)
  • OpenAPI doc — OpenAPI документация сгенерированная
  • API Client 1...N — клиенты потребители данных API (могут быть реализованы на любом ЯП, который поддерживает генератор, для NSwag C# и Typescript)
  • SPA 1...N — Фронтенд приложение, в нашем случае React (NSwag поддерживает генерацию клиентов для AngularJS/Angular, React (fetch), JQuery(Promise/Callback), Aurelia)

Последовательность действий следующая:

  • Пометить API контроллер соответствующим тегом/атрибутом/декоратором зависит от ЯП, на котором реализовано API
  • Сгенерировать OpenAPI документацию по коду API
  • Сгенерировать код API клиента по OpenAPI документации
  • Интегрировать код API клиента в приложение потребитель данных API

Документация


Для того чтобы получить возможность генерации кода, нам необходимо описать типы принимаемых/возвращаемых значений контроллера API, в данном примере, где использован язык C#, с помощью атрибута SwaggerOperation мы пометили метод который будет возвращать список всех опросников на сервере, в коде клиентов метод для получения данных будет называться GetAllQuestionnaires:

[HttpGet, Route("")]
[SwaggerOperation(OperationId = "GetAllQuestionnaires")]
[SuccessResponse(typeof(IEnumerable<QuestionnaireViewModel>))]       
 public IEnumerable<QuestionnaireViewModel> Get()
 {
   var surveys = _questionnaireRepository.GetAll();
   return surveys.Select(QuestionnaireViewModel.ToViewModel).ToArray();
 }

Листинг 1 Пример С# кода, описывающего метод API

Затем с помощью NSwag мы автоматически генерируем OpenAPI документ, который будет содержать все API эндпоинты которые были помечены соответствующими атрибутами в коде бекенда.


Рисунок 2 OpenAPI документ

Таким образом у нас получилось создать всегда актуальную автоматически обновляемую документацию нашего API.

Типизация


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

Для нашего примера документ содержит информацию о типе QuestionnaireViewModel (здесь спецификация представлена в HTML виде для удобства чтения)


Рисунок 3 Пример модели данных в OpenAPI документе

Следующий шаг — это передать эту информацию в код фронтенд приложения.

Генерация кода


Для генерации кода API клиента мы также используем NSwag. На вход он принимает OpenAPI документ и генерирует код API клиента в соответствии с заданными настройками. Для фронта рядом с полученным кодом мы добавляем package.json и отправляем в наш локальный npm регистр.

Как видно из листинга кода бэкенда (см. листинг 1), мы пометили метод контроллера с помощью атрибута

[SwaggerOperation(OperationId = "GetAllQuestionnaires")] 

OperationId заданный в атрибуте C# в нашем случае станет именем метода клиента.


Рисунок 4 Пример использования сгенерированного API клиента

Также после генерации клиента мы получили d.ts файл который содержит соответствующие описания типов данных, показан на рисунке ниже.


Рисунок 5 Пример описания типа данных в .d.ts файле

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


Рисунок 6 Пример использования информации о типе данных API клиента

Также работают все соответствующие валидаторы типов данных в Typescript.

Пример на рисунках ниже.


Рисунок 7 Пример валидации типа данных API клиента


Рисунок 8 Пример валидации типа данных API клиента

Выводы


После применения данного подхода мы получили следующие преимущества:

  • Меньше багов связанных с типами данных
  • Меньше кода, меньше трудозатрат на отладку, тестирование, поддержку
  • Всегда актуальная документация для всех методов API для каждого из микросервисов
  • Возможность автоматизации тестирования API
  • Единая система типов для фронтенда и бэкенда


Ссылки
  • Спецификация OpenAPI
  • Сайт проекта NSWAG
Поделиться публикацией

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 21

    0

    А тайп-чеккинг ответов с бэка на фронте проводится в рантайме? Или они принудительно приводятся к соответвующему типу, предлагая, что бэк иного прислать не может никогда и ни при каких условиях?

      +1

      В рантайме нет никаких проверок, если бек вдруг пришлёт что то не то, то это считается багом и надо искать причины и чинить или бек или фронт

        0

        Как выявляется, что пришло что-то не то?

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

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

              0

              Я к тому, что если тайпгвардов полноценных нет, то гарантии типов мало чего стоят.

                0
                гарантии типов мало чего стоят

                Почему вы так считаете?
                  0

                  Ну какие могут быть гарантии, если где-то в коде что-то вроде:


                  async function getCurrentUser(): Promise<User> {
                    const response = await axios.get('/me')
                    return response.json as User;
                  }
                    0

                    Так, а зачем вы такой код пишете?)

                      0

                      Это вы такой генерируете как я понимаю: "В рантайме нет никаких проверок"

                      +1

                      Так это же автогенеренный код, по типам с бека, которые проверены компилятором. Если эндпоинт вернет что-то не то — это ошибка в компиляторе бека или самом кодогенераторе, других вариантов нет. Если генерить рантайм-проверки то там может быть такая же ошибка, т.е. статические проверки уже дают максимум гарантий. Больше никак не получить.

                        0

                        Ещё может быть банальная рассинхронизация версий схем, ожидаемых клиентом и реально отправляемых сервером.

                          0
                          Ещё может быть банальная рассинхронизация версий схем, ожидаемых клиентом и реально отправляемых сервером.

                          Откуда она может взяться? Бек обновлен => типы перегенерены. С-но, типы всегда актуальные.

                            0

                            То, что они перегенерировались ещё не значит, что на фронте они актуальны. Особенно если фронтов несколько.

                              0
                              То, что они перегенерировались ещё не значит, что на фронте они актуальны.

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

                0
                Как выше заметили все это делается не для рантайма, если все контракты описаны и по ним созданы клиенты, то смысл проверки в рантайме просто отпадает.
            0

            Использую такой же подход, очень удобно.


            Для упрощения генерации кода по спецификации OpenAPI из Visual Studio (как для фронтенда на C# и TypeScript, так и бэкенда на C#) могу посоветовать расширение Unchase OpenAPI Connected Service, которое использует актуальный NSwag.
            Инструкция по использованию на medium.com.

              0
              Все это красиво, но!
              У нас был сервис, который требовал отдавать клиенту ответ со стасом ошибка или нет. Если ошибка — код ошибки и сообщение. Иначе — ответ с объектом или результатом.
              Возникли приколы с попыткой отдавать изображения. Плюс были приколы с разными типами ошибок. Но все пришло в печаль, когда клиент рос и стал размером в 1000+ методов. Представьте насколько это удобно искать среди кучи методов необходимый Вам? Представили? А теперь добавьте еще 2-3 таких сервиса. Вуаля. Вас ненавидят (но это не точно). Поэтому от OperationId мы отказались. Также при инициализации клиента не заметил использования авторизации. У Вас сервис для внутренного использования или же просто это для теста?
                0

                Может, когда у сервиса 1000+ ендпоинтов, то надо их как-то разделить, если не физически (на микросервисы), то логически? И генерировать/писать клиентов типа UserServiceClient, OrderServiceClient и т. п., которые оформлять как отдельные пакеты, и каждое приложение ставит себе только нужные ему, а н еодного клиента, который знает о всех-всех ендпоинтах на проекте, а то и в компании.

                  0
                  Также при инициализации клиента не заметил использования авторизации. У Вас сервис для внутренного использования или же просто это для теста?

                  Мы используем метод transformOptions, который проставляет Authorization заголовок в http headers. В статье я не стал описывать этот момент.
                  Документация на сайте NSWAG
                    0
                    Но все пришло в печаль, когда клиент рос и стал размером в 1000+ методов. Представьте насколько это удобно искать среди кучи методов необходимый Вам? Представили?

                    Это печально конечно иметь сервис с 1000+ ендпоинтов, но мне кажется это вопрос организации кода и общего взаимодействия между сервисами и системами.
                    Я думаю что можно было сделать иначе, кроме как накидать 1000+ ендпоинтов в один микросервис.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое