Автогенерация функций выборки данных и всей сопутствующей типизации с помощью Orval

Требования к быстрому и качественному созданию интерфейсов растут с каждым днем. Поэтому разработчики плавно отходят от написания вручную кода, который может быть сгенерирован автоматически. Наша команда перешла к автоматизации с таким инструментом, как 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 - генерация директории для каждого тега в целевой папке и разделение ее на несколько файлов.

Теперь рассмотрим полный флоу интеграции и пример сгенерированного кода.

1. Устанавливаем orval в проект.

  • yarn add -D orval или npm i –save-dev orval в зависимости от используемого менеджера пакетов.

2. Создаём конфигурационный файл 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', }, }, }, }, })

3. Добавляем в проект мутатор, если он вам необходим. Вы можете ограничиться стандартными клиентом выборки данных из числа предлагаемых самим 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 под реалии своего проекта, вы сэкономите кучу времени, которое лучше потратить на оптимизацию или рефакторинг.

Обязательно используйте его для:

  • Крупных проектов с большим количеством эндпоинтов – ваши фронтендеры избавятся от необходимости вручную писать повторяющийся код и станут не только свободнее для более приоритетных задач, но и счастливее;
  • Команд, в которых работает сразу несколько разработчиков – Orval генерирует стандартизированный код, что помогает поддерживать единообразие и упрощает работу с кодовой базой;
  • Кастомизации под другие проекты – инструмент можно адаптировать под конкретные нужды проекта, включая трансформацию данных, фильтрацию эндпоинтов и другие настройки.

Обновлять API также станет проще и быстрее: когда спецификация меняется, Orval позволяет быстро сгенерировать обновленные функции и типы, сокращая риск появления устаревшего или некорректного кода в проекте.

Начать дискуссию