Generate API Call with Orval and Axios from OpenAPI Schema
Friday, November 14, 2025
I have a project with .NET Core 9.0 backend and React frontend. With the built-in OpenAPI support via Microsoft.AspNetCore.OpenApi, it makes it so much easier to generate OpenAPI documents. For more information, visit https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/overview?view=aspnetcore-9.0.
To automate the process, I want to generate the document after built. And that's pretty easy as well by following instructions in (Generate OpenAPI documents at build-time)https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-9.0&tabs=visual-studio%2Cvisual-studio-code#generate-openapi-documents-at-build-time article.
In my csproj file, I put the following entries, so the Open API documents can be exported to a common location outside of the backend project directory with name api.json.
<PropertyGroup>
<OpenApiDocumentsDirectory>../../../openapi</OpenApiDocumentsDirectory>
<OpenApiGenerateDocumentsOptions>--file-name api</OpenApiGenerateDocumentsOptions>
...
</PropertyGroup>
On the frontend, I would like to automatically ingest that document and automatically generate code to make the api call. That's where Orval (https://orval.dev/) comes in. Since I want to use custom Axios instance, I use the following guide: https://orval.dev/guides/custom-axios. My orval.config.js looks like the following:
import { defineConfig } from 'orval';
export default defineConfig({
api: { //custom name, it can be any friendly name.
input: '../openapi/api.json',
output: {
mode: 'single',
target: 'src/api/api.ts',
schemas: 'src/api/model',
mock: false,
override: {
mutator: {
path: './src/services/axios-service.ts',
name: 'request'
}
}
}
}
});
The input is the path to the Open API document. Within output, mode: Single tells Orval to generate a single file for all calls (usually one function per endpoint): https://orval.dev/reference/configuration/output#value-single. And target is the output location and name of the generated code. While schemas will hold the generated models/DTOs that represents request and response data. I just turn mock to false since I don't use Faker at this time https://orval.dev/reference/configuration/output#mock.
The override is where the interesting thing happen. In my case, it basically says, use the request function in ./src/services/axios-service.ts to make the call.
My axios-service.ts contains the following:
export const request = <T>(
config: AxiosRequestConfig,
params?: Params
): Promise<T> => {
axiosInstance.defaults.baseURL = import.meta.env.VITE_BASE_URL;
const source = axios.CancelToken.source();
const axiosConfig: AxiosRequestConfig = {
...config,
cancelToken: source.token,
headers: {
Authorization: `Bearer ${params?.accessToken}`
}
};
const promise = axiosInstance(axiosConfig).then(({ data }) => data);
// @ts-ignore
promise.cancel = () => {
source.cancel('Query was cancelled');
};
return promise;
};
This way, I use the BASE_URL specified in .env file so I can differentiate between local and remote environment. Also, I allow injection of access token, so my call can be authorized by the backend.
Orval will then generate something similar to the following calls in api.ts:
options?: SecondParameter<typeof request<UserResponse>>,) => {
return request<UserResponse>(
{url: `/users`, method: 'POST'
},
options);
}
export const getCustomers = (
options?: SecondParameter<typeof request<CustomersResponse[]>>,) => {
return request<CustomersResponse[]>(
{url: `/customers`, method: 'GET'
},
options);
}
export const postCustomers = (
customersRequest: CustomersRequest,
options?: SecondParameter<typeof request<void>>,) => {
return request<void>(
{url: `/customers`, method: 'POST',
headers: {'Content-Type': 'application/json', },
data: customersRequest
},
options);
}
In package.json, I added command to run orval during npm run dev and npm run build commands:
::
That way, when I run the frontend locally, it will generate the code using the latest document. It will also generate when run within CI/CD pipeline. It saves a lot of time by automatically modify the frontend code when the backend api changes.