Автогенерация функций выборки данных и всей сопутствующей типизации с помощью Orval
Требования к быстрому и качественному созданию интерфейсов растут с каждым днем. Поэтому разработчики плавно отходят от написания вручную кода, который может быть сгенерирован автоматически. Мы перешли к автоматизации с таким инструментом, как Orval. Расскажем, как это было, поделимся примером кода и библиотеками (следите за ссылками в тексте).
Почему мы отказались от ручной выборки данных?
Правило нашей команды: если рутинные процессы могут быть успешно автоматизированы, мы обязательно так и сделаем. И потратим свободное время на куда более приоритетные вещи, чем написание повторяющегося кода из проекта в проект. Например, на дополнительную оптимизацию приложений.
Большинство наших проектов состоит из множества CRUD-ов, а количество запросов может превышать сотню. Ранее мы описывали запросы выборки данных и всю относящуюся к ним типизацию вручную. Выглядеть это могло так:
const getVacanciesData = async ({
locale,
}: ServiceDefaultParams): Promise> => {
try {
const response: JsonResponse = await get({
url: VACANCIES_ENDPOINT,
headers: { ...getXLangHeader(locale) },
});
return { ok: true, data: response?.data || [] };
} catch (e) {
handleError(e);
return { ok: false, data: undefined };
}
};
export default getVacanciesData;
Ранее мы написали оптимизированное API для отправки запросов на сервер на базе axios. Весь код с примерами сервисов на базе данного API вы сможете найти в другой нашей статье. К слову, метод get, используемый на скриншоте выше относится к данному API.
Главный минус, помимо времени: высокая вероятность допустить ошибки при создании подобных запросов. Например, при настройке опциональности внутри типов или неправильной передаче тела запроса. А в случае с автогенерацией, ошибка может быть ТОЛЬКО со стороны сервера — код опирается на yaml-файл, созданный бэкенд-разработчиком, поэтому ответственность лежит исключительно на одной стороне.
Создание тривиальных запросов на фронте буквально занимает 0 секунд.А единственный нюанс, с которым мы столкнулись за все время использования автогенерации, — модифицикация существующих запросов. А именно — создание прослойки в виде адаптора. Но она требуется не всегда.
Так использование Orval для генерации сервисов помогает сэкономить время и исключить вероятность возникновения ошибок на стороне фронтенда.
Почему Orval?
Далее мы рассмотрим самые важные настройки Orval и узнаем, как интегрировать автогенерацию в наше приложение.
Orval — это инструмент для генерации клиентского кода для RESTful API на основе OpenAPI-спецификаций. С его официальной документацией можно ознакомиться по ссылке.
Для базовой настройки поведения Orval достаточно просто создать конфигурационный файл в корне проекта. Выглядит он так — orval.config.js
Один из ключевых параметров конфигурации — это input. В orval.config.js он указывает на источник спецификации OpenAPI и включает различные опции для его настройки.
Давайте рассмотрим его подробнее.
Input
Данная часть конфигурации отвечает за импортирование и преобразование используемого OpenAPI-файла.
target — обязательный параметр, который содержит путь до openapi файла, из которого будут сгенерированы сервисы.
validation — параметр, отвечающий за использование линтера openapi-validator для openapi, разработанного IBM. По-умолчанию имеет значение false. Включает в себя стандартный набор правил, по желанию их можно расширить в .validaterc файле.
override.transformer — путь до файла, импортирующего функцию-трансформер, либо же сама функция-трансформер. Функция принимает первым параметром OpenAPIObject и должна возвращать объект с такой же структурой.
filters — принимает в себя объект с ключом tags, в который необходимо передать массив со строками, либо регулярным выражением. Будет произведена фильтрация по тегам, если они есть в openapi схеме. В случае если теги не найдены — генерация вернет пустой файл с заголовком и версией.
Output
Данная часть конфигурации отвечает за настройку генерируемого кода.
workspace — общий путь, который будет использоваться в последующих заданных путях внутри output.
target — путь к файлу, который будет включать сгенерированный код.
client — название клиента выборки данных, либо ваша собственная функция с реализацией.(angular, axios, axios-functions, react-query, svelte-query, vue-query, swr, zod, fetch.)
schemas — путь, по которому будут сгенерированы типы TS. (по-умолчанию типы генерируются в файле, указанном в target)
mode — способ генерации конечных файлов.
single — один общий файл, который включает в себя весь сгенерированный код.
split — разные файлы для запросов и типизации
tags — генерация собственного файла для каждого тега из openapi.
tags-split — генерация директории для каждого тега в целевой папке и разделение ее на несколько файлов.
Теперь рассмотрим полный флоу интеграции и пример сгенерированного кода.
Устанавливаем orval в проект.
Создаём конфигурационный файл orval.config.js в корне проекта.
import { defineConfig } from 'orval'
export default defineConfig({
base: {
input: {
target: 'https://your-domen/api.openapi',
validation: true,
},
output: {
target: './path-to-generated-file/schema.ts',
headers: true,
prettier: true,
mode: 'split',
override: {
mutator: {
path: './path-to-your-mutator/fetch.ts',
name: 'customInstance',
},
},
},
},
})
Добавляем в проект мутатор, если он вам необходим. Вы можете ограничиться стандартными клиентом выборки данных из числа предлагаемых самим Orval: Angular, Axios, Axios-functions, React-query, Svelte-query, Vue-query, Swr, Zod, Fetch.
Мы же написали свой собственный, который подходит для использования в последних версиях Next.js. Вот его код:
import { getCookie } from 'cookies-next'
import qs from 'qs'
import { AUTH_TOKEN } from '../constants'
import { deleteEmptyKeys } from '../helpers'
import type { BaseRequestParams, ExternalRequestParams } from './typescript'
const API_URL = process.env.NEXT_PUBLIC_API_URL
const validateStatus = (status: number) => status >= 200 && status <= 399
const validateRequest = async (response: Response) => {
try {
const data = await response.json()
if (validateStatus(response.status)) {
return data
} else {
throw { ...data, status: response.status }
}
} catch (error) {
throw error
}
}
export async function customInstance(
{ url, method, data: body, headers, params = {} }: BaseRequestParams,
externalParams?: ExternalRequestParams
): Promise {
const baseUrl = `${API_URL}${url}`
const queryString = qs.stringify(deleteEmptyKeys(params))
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl
const requestBody = body instanceof FormData ? body : JSON.stringify(body)
const authToken = typeof window !== 'undefined' ? getCookie(AUTH_TOKEN) : null
const requestConfig: RequestInit = {
method,
headers: {
'Content-Type': 'application/json',
Accept: 'application/json',
...(authToken && { Authorization: `Bearer ${authToken}` }),
...headers,
...externalParams?.headers,
},
next: {
revalidate: externalParams?.revalidate,
tags: externalParams?.tag ? [externalParams?.tag] : undefined,
},
body: ['POST', 'PUT', 'PATCH'].includes(method) ? requestBody : undefined,
}
try {
const response = await fetch(fullUrl, requestConfig)
return await validateRequest(response)
} catch (error) {
console.error(`Request failed with ${error.status}: ${error.message}`)
throw error
}
}
Сгенерированные сервисы выглядят так:
/**
* @summary Get config for payout
*/
export const getConfigForPayout = (options?: SecondParameter) => {
return customInstance({ url: `/api/payout/config`, method: 'GET' }, options)
}
/**
* Method blocks specified user's balance for payout
* @summary Request payout action
*/
export const requestPayoutAction = (
requestPayoutActionBody: RequestPayoutActionBody,
options?: SecondParameter
) => {
return customInstance(
{
url: `/api/payout/request`,
method: 'POST',
headers: { 'Content-Type': 'application/json' },
data: requestPayoutActionBody,
},
options
)
}
Обратите внимание на функцию customInstance — это мутатор, в который Orval передаёт все необходимые данные. Вы можете реализовать эту функцию, как вам нужно. Главное правильно принять входные параметры.
Сгенерированная типизация выглядит так:
export type GetConfigForPayoutResult = NonNullable>>
export type GetConfigForPayout200DataRestrictions = {
max_amount: number
min_amount: number
}
export type GetConfigForPayout200DataAccount = {
created_at: string
id: number
type: string
}
export type GetConfigForPayout200Data = {
account?: GetConfigForPayout200DataAccount
balance: number
restrictions: GetConfigForPayout200DataRestrictions
}
export type GetConfigForPayout200 = {
data?: GetConfigForPayout200Data
}
OpenAPI спецификация для данных сервисов выглядит так:
/api/payout/config:
get:
summary: 'Get config for payout'
operationId: getConfigForPayout
description: ''
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
data:
balance: 180068.71618
restrictions:
max_amount: 63012600.110975
min_amount: 22.2679516
account:
id: 20
type: eum
created_at: '1970-01-02T03:46:40.000000Z'
properties:
data:
type: object
properties:
balance:
type: number
example: 180068.71618
restrictions:
type: object
properties:
max_amount:
type: number
example: 63012600.110975
min_amount:
type: number
example: 22.2679516
required:
- max_amount
- min_amount
account:
type: object
properties:
id:
type: integer
example: 20
type:
type: string
example: eum
created_at:
type: string
example: '1970-01-02T03:46:40.000000Z'
required:
- id
- type
- created_at
required:
- balance
- restrictions
tags:
- Payout
/api/payout/request:
post:
summary: 'Request payout action'
operationId: requestPayoutAction
description: "Method blocks specified user's balance for payout"
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
data: null
properties:
data:
type: string
example: null
tags:
- Payout
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
type:
type: string
description: ''
example: withdrawal
enum:
- withdrawal
method_id:
type: integer
description: 'Must be at least 1.'
example: 12
amount:
type: number
description: 'Must be at least 0.01. Must not be greater than 99999999.99.'
example: 17
required:
- type
- method_id
- amount
После настройки автогенерации всё, что нам необходимо для удобного использования, — документация, где мы смотрим наименование необходимого сервиса, а затем используем автоимпорт.
Сперва мы внедрили данную конфигурацию в один из наших проектов и вносили корректировки, опираясь на полученные проблемы. Когда мы убедились, что все работает, как и планировалось, то начали использовать Orval для автоматической генерации функций выборки данных на всех новых проектах. Познакомиться с ними можно здесь.
Почему вам нужна автогенерация?
Один раз настроив Orval под реалии своего проекта, вы сэкономите кучу времени, которое лучше потратить на оптимизацию или рефакторинг.
Обязательно используйте его для:
Крупных проектов с большим количеством эндпоинтов — ваши фронтендеры избавятся от необходимости вручную писать повторяющийся код и станут не только свободнее для более приоритетных задач, но и счастливее;
Команд, в которых работает сразу несколько разработчиков — Orval генерирует стандартизированный код, что помогает поддерживать единообразие и упрощает работу с кодовой базой;
Кастомизации под другие проекты — инструмент можно адаптировать под конкретные нужды проекта, включая трансформацию данных, фильтрацию эндпоинтов и другие настройки.
Обновлять API также станет проще и быстрее: когда спецификация меняется, Orval позволяет быстро сгенерировать обновленные функции и типы, сокращая риск появления устаревшего или некорректного кода в проекте.