В нашей компании в процессе разработки frontend‑приложений мы иногда сталкиваемся с одной из следующих ситуаций (или с обеими сразу):
- Когда мы имеем достаточно объемную и часто меняющуюся спецификацию API. Тут нам поможет генерация кода на основе этой спецификации.
- Когда нам нужно работать с функционалом, отвечающим за обработку обращений к различным эндпойнтам, но сами запросы не работают по каким‑либо причинам. Эту проблему можно решить подстановкой в соответствующих местах mock‑объектов.
В данной статье мы поговорим и о генерации клиентского кода на основе спецификации OpenAPI, и о мокировании запросов.
Что конкретно мы будем делать:
- научимся генерировать клиент API для Axios и TypeScript на основе спецификации;
- взглянем на типизацию на основе спецификации;
- настроим Mock Service Worker (MSW) для перехвата запросов;
- поработаем с библиотеками Faker и Source для генерации данных.
Когда это нужно
Для начала рассмотрим некоторые случаи, когда подход и инструменты, о которых пойдет речь ниже, могут нам пригодиться. 1. Перехват запросов воркером поможет если сервер по каким‑либо причинам не доступен. Это могут быть сбои в работе сети, отключение непосредственно сервера или простой во время профилактики. 2. Бывают кейсы, когда контракт уже есть, но новый функционал сервера еще не развернут на стенде. 3. Нам нередко нужно проверить интерфейс на прочность, а данных для этого не хватает. Можно добавить данные вручную, но это может вылиться в нерациональную трату времени. 4. При написании тестов часто проще опираться на заранее подготовленные данные, а также на заглушки запросов. 5. Мокирование данных может расширить возможности различных инструментов, таких как, например, Storybook.
И это не все сценарии. В конце концов, мы всегда можем решить провести какой‑нибудь эксперимент для проверки нашей теории, а мокирование API избавляет от необходимости готовить для этого backend. Достаточно дописать несколько строк в спецификацию.
Если сервер для разработки разворачивается на нашей машине или, например, запускается в контейнере, некоторые приемы из данной статьи могут быть неактуальны, так что все ситуативно.
Проект и контракт для примера
В качестве тестового проекта будем использовать базовый проект Vue 3, из которого мы удалим все компоненты кроме App.vue
. Все фрагменты кода будут приведены в слегка упрощенном виде для наглядности. Расположение файлов не будет иметь никакого принципиального значения. В реальных же проектах всегда следует отталкиваться от принятой файловой структуры и руководствоваться архитектурными решениями, и если вы будете пробовать что‑то у себя на проекте, не забудьте проверить пути к файлам.
Описанные в статье инструменты являются достаточно универсальными. Я, например, применяю их на проектах Vue и React, а клиент нужен не только под Axios, но иногда и под нативный fetch. Принципы интеграции не меняются.
Разбираться с генерацией и мокированием будем на примере небольшой спецификации, которая описывает операции CRUD для сервиса управления списком пользователей. Спецификация приведена в формате JSON, потому что для одного из инструментов мокирования, который мы будем рассматривать, требуется именно такой (а для генерации можно брать файл и в формате YAML). Файл spec.json мы положим в папку openapi в корне проекта.
Генерация клиента
Установка OpenAPI Generator
Для начала нам нужно установить OpenAPI Generator CLI. У нас есть несколько вариантов установки. Если генератор используется постоянно и в разных проектах, можно установить его глобально. Мы же пойдем по самому простому пути и добавим пакет в проект как dev зависимость:
npm i @openapitools/openapi-generator-cli -D
Если есть необходимость генерировать новый клиент периодически, можно добавить скрипт в раздел scripts
в package.json, например:
"generate-api": "openapi-generator-cli generate -i openapi/spec.json -g typescript-axios -o src/api/api"
Генерация клиентского кода
Для получения готового к работе клиента из спецификации нужно выполнить команду:
npx openapi-generator-cli generate -i openapi/spec.json -g typescript-axios -o src/api/api
Мы воспользовались тремя основными опциями команды generate
:
* -i
(или --input-spec
) — источник, то есть наш файл со спецификацией;
* -g
(или --generator-name
) — генератор, который мы хотим использовать, в нашем примере это будет typescript-axios
(чуть ниже расскажу почему);
* -o
(или --output
) — указывает папку, куда нужно положить готовый клиент.
На выходе получим файл конфигурации openapitools.json в корне проекта и папку с файлами клиента по указанному пути. Содержимое папки крайне не рекомендуется править вручную. Последующая генерация перетрет эти правки.
В Windows 10 в некоторых случаях выполнение команды может завершиться ошибкой, связанной с Java Runtime, а папка с клиентом не появится. В таких случаях можно в файле конфигурации понизить версию генератора до 6.6.0 и выполнить команду еще раз. Правда в этом случае в версиях OpenAPI выше 3.0.4 может некорректно сгенерироваться типизация... Я потратил несколько вечеров, разбираясь с этой проблемой, но так ни к чему определенному и не пришел. Если у вас есть решение, поделитесь, пожалуйста, в комментариях.
При необходимости можно использовать содержимое папки как шаблон для подготовки клиента к публикации в виде пакета npm во внутренний реестр, например, в Nexus или Verdaccio.
Например, мы добавили на одном из наших проектов несколько этапов в GitLab CI, которые отрабатывают после обновления спецификации (примерно как на изображении ниже): * генерация клиента на основе обновленной спецификации; * добавление нужных полей в package.json; * публикация в Nexus новой версии клиента; * отправка уведомления в чат разработчикам о появлении в реестре нового клиента.
Так мы упростили коммуникацию между разработчиками и сократили сроки добавления нового функционала на стороне frontend.
Интеграция клиента
Настройка Axios
Добавим Axios в проект:
npm i axios
Теперь у нас есть все, чтобы связать клиент с проектом. Для этого создадим в проекте файл src/api/index.ts:
// src/api/index.ts
import axios, { AxiosError, isAxiosError } from 'axios'
import { UsersApi } from './api'
const axiosInstance = axios.create()
axiosInstance.interceptors.request.use((config) => {
const token = 'SOME_BEARER_TOKEN'
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
})
axiosInstance.interceptors.response.use(undefined, (error: AxiosError) => {
if (!isAxiosError(error)) {
return
}
if (error.response?.status === 401) {
console.warn('401 Unauthorized')
}
return Promise.reject(error)
})
export const usersApi = new UsersApi(undefined, 'https://127.0.0.1:20000/api', axiosInstance)
Давайте разберемся, что тут происходит, и при чем тут Axios.