Autogeneration of data fetching functions and typing with Orval
Contents
The need for fast, high quality interface creation is increasing every day. As a result, developers are moving away from manually writing code that can be generated automatically. We have moved towards automation with a tool like Orval. We'll tell you how we did it, and share sample code and libraries (follow the links in the text).
Why we have abandoned manual data fetching
Our team philosophy is that if routine processes can be successfully automated, we will do it. And we will spend our free time on much higher priority things than writing repetitive code from project to project. For example, additional application optimisation.
Most of our projects consist of multiple CRUDs, and the number of queries can exceed a hundred. In the past, we used to manually describe data fetching queries and all the typing involved. It could look like this:
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;
Earlier we wrote an optimized API for sending requests to axios-based servers. You can find all the code with examples of services based on this API in our other article. By the way, the get method used in the screenshot above is part of this API.
The main disadvantage, apart from wasting time, is the high probability of making mistakes when creating such queries. For example, when setting optionality within types, or incorrectly passing the request body. And in the case of autogeneration, the error can ONLY be on the server side – the code relies on the yaml file created by the backend developer, so the responsibility is on one side.
Creating trivial queries on the front-end takes literally 0 seconds. And the only nuance we encountered during the whole time we were using autogeneration was the modification of existing queries. Namely, the creation of a layer in the form of an adapter. But this is not always necessary.
Using Orval for service generation therefore helps to save time and eliminate the possibility of errors on the front end.
Why is Orval
Next, let's look at the most important Orval settings and learn how to integrate autogeneration into our application.
Orval is a tool for generating client code for RESTful APIs based on OpenAPI specifications. Its official documentation can be found here.
For the basic configuration of Orval's behavior, it is enough to create a configuration file in the root of the project. It looks like this – orval.config.js
One of the most important configuration parameters is input. In orval.config.js it points to the source of the OpenAPI specification and contains various configuration options.
Let's have a look at it.
Input
This part of the configuration is responsible for importing and converting the OpenAPI file used.
target - required parameter that specifies the path to the OpenAPI file from which services are generated;
validation - parameter responsible for using the openapi-validator lint developed by IBM for OpenAPI. The default value is false. Includes a default set of rules which can be optionally extended in the .validaterc file;
override.transformer - OpenAPIObject as its first parameter and must return an object with the same structure;
filters - accepts an object with the tags key, to which an array of strings or a regular expression should be passed. If there are tags in the OpenAPI schema, it will filter by tags. If no tags are found, generation will return an empty file with title and version.
validation - parameter responsible for using the openapi-validator lint developed by IBM for OpenAPI. The default value is false. Includes a default set of rules which can be optionally extended in the .validaterc file;
override.transformer - OpenAPIObject as its first parameter and must return an object with the same structure;
filters - accepts an object with the tags key, to which an array of strings or a regular expression should be passed. If there are tags in the OpenAPI schema, it will filter by tags. If no tags are found, generation will return an empty file with title and version.
Output
This part of the configuration is responsible for setting up the generated code.
workspace - common path that will be used in subsequent specified paths within the output;
target - path to file containing the generated code;
client - the name of the data fetch client, or your own function with implementation (angular, axios, axios-functions, react-query, svelte-query, vue-query, swr, zod, fetch.);
schemas - path where TS types are generated (by default, types are generated in the file specified in target);
mode - a method of generating final files that includes the following options
target - path to file containing the generated code;
client - the name of the data fetch client, or your own function with implementation (angular, axios, axios-functions, react-query, svelte-query, vue-query, swr, zod, fetch.);
schemas - path where TS types are generated (by default, types are generated in the file specified in target);
mode - a method of generating final files that includes the following options
- single - a common file containing all generated code;
- split -different files for querying and typing;
- tags - the generation of a separate file for each tag from the OpenAPI;
- tags-split - the generation of a directory for each tag in the destination folder and splitting of it into several files.
Now let's look at the whole integration path and an example of the generated code.
1
Install Orval in the project:
- yarn add -D orval or npm i –save-dev orval – depending on the package manager you use
2
Create a configuration file orval.config.js in the root of the project.
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
Add a mutator to the project if you need to use one. You can use the standard data sampling client among those offered by Orval itself: Angular, Axios, Axios-functions, React-query, Svelte-query, Vue-query, Swr, Zod, Fetch.
We wrote our own, which is suitable for use in the latest versions of Next.js. Here is its code:
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
}
}
The generated services look like this:
/**
* @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
)
}
Note the customInstance function – this is a mutator to which Orval passes all the necessary data. You can implement this function however you need. The main thing is to accept the input parameters correctly.
The generated typing will look something like this:
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
}
The OpenAPI specification for these services looks like this:
/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
Once autogeneration is set up, all we need for usability is documentation where we look up the name of the service we need and then use auto-import.
We first implemented this configuration in one of our projects and made adjustments based on the problems we encountered, then when we were sure that everything was working as planned, we started to extend this configuration to our other projects. We now use Orval on all new projects to automatically generate data sampling functions. Examples provided here.
Why do you need autogeneration
Once you have adapted Orval to the realities of your project, you will save a lot of time that could be better spent on optimisation or refactoring.
It's necessary for use in:
- Large projects with lots of endpoints – your front-end users will no longer have to manually write repetitive code, leaving them not only freer to work on higher priority tasks, but also happier;
- Multi-developer teams – Orval generates standardized code, which helps maintain consistency and makes it easier to work with the code base;
- Customisation for other projects – the tool can be customized to meet specific project requirements, including data transformation, endpoint filtering and other customisations.
Updating the API is also easier and faster: when the specification changes, Orval allows you to quickly generate updated functions and types, reducing the risk of outdated or incorrect code in your project.
Tags:
You may also like
Integrating Unity code into React Native. Part II
Ilya Mazhugin, mobile developer
React Native
28.06.202414 minutes
How to make a cart and authorization in an online store
Max Bantsevich, CEO
E-commerce
Grocery Solutions
11.03.20245 minutes
CSS and Next.js updates, web accessibility principles, dynamic font scaling and efficient typing for redux-thunk
Artur Valokhin, lead frontend developer
Next.js
26.09.202315 minutes
Using BLE technology when working with beacon in React Native
Maria Sidorevich, Lead Mobile Developer
Foodtech
Delivery Management
React Native
BLE technology
22.03.202415 minutes
Basic REST API for sending requests to the server using Axios
Artur Valokhin, lead frontend developer
JavaScript
28.07.20239 minutes
What is the essence of RFID and NFC? And how to use them in the foodtech industry?
Ilya Mazhugin, mobile developer
Security
Payment
Foodtech
15.12.202313 minutes