Orval takes OpenAPI files, interprets them, and converts them into TypeScript code, generating client code as well. You can customize its behavior by writing settings in orval.config.js.
In orval.config.js, the override.mutator allows you to specify a mutator, enabling the generation of client code using a custom HTTP client (like fetch or axios) by specifying the mutator's path and name. It looks something like this. (This is written with the assumption that react-query is being used.)
https://orval.dev/reference/configuration/output#mutator
module.exports = {
petstore: {
output: {
override: {
mutator: {
path: './api/mutator/use-custom-instance.ts',
name: 'useCustomInstance',
// default: true
},
},
},
},
};
import Axios, { AxiosRequestConfig } from 'axios';
import { useQueryClient } from 'react-query';
export const AXIOS_INSTANCE = Axios.create({ baseURL: '' });
export const useCustomInstance = <T>(): ((
config: AxiosRequestConfig,
) => Promise<T>) => {
const token = useToken(); // Do what you want
return (config: AxiosRequestConfig) => {
const source = Axios.CancelToken.source();
const promise = AXIOS_INSTANCE({
...config,
headers: {
Authorization: `Bearer ${token}`
}
cancelToken: source.token,
}).then(({ data }) => data);
// @ts-ignore
promise.cancel = () => {
source.cancel('Query was cancelled by React Query');
};
return promise;
};
};
export default useCustomInstance;
export type ErrorType<Error> = AxiosError<Error>;
Then, the code is generated like this.
import { useMutation, useQuery } from "@tanstack/react-query";
import type {
MutationFunction,
QueryFunction,
QueryKey,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from "@tanstack/react-query";
import { useCallback } from "react";
import type { Error, ListUsersParams, User } from "../schemas";
import { useCustomInstance } from "../../lib/axios";
/**
* @summary List all users
*/
export const useListUsersHook = () => {
const listUsers = useCustomInstance<User[]>();
return useCallback(
(params?: ListUsersParams, signal?: AbortSignal) => {
return listUsers({ url: `/users`, method: "GET", params, signal });
},
[listUsers]
);
};
export const getListUsersQueryKey = (params?: ListUsersParams) => {
return [`/users`, ...(params ? [params] : [])] as const;
};
export const useListUsersQueryOptions = <
TData = Awaited<ReturnType<ReturnType<typeof useListUsersHook>>>,
TError = unknown,
>(
params?: ListUsersParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<ReturnType<typeof useListUsersHook>>>,
TError,
TData
>;
}
) => {
const { query: queryOptions } = options ?? {};
const queryKey = queryOptions?.queryKey ?? getListUsersQueryKey(params);
const listUsers = useListUsersHook();
const queryFn: QueryFunction<
Awaited<ReturnType<ReturnType<typeof useListUsersHook>>>
> = ({ signal }) => listUsers(params, signal);
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<ReturnType<typeof useListUsersHook>>>,
TError,
TData
> & { queryKey: QueryKey };
};
export type ListUsersQueryResult = NonNullable<
Awaited<ReturnType<ReturnType<typeof useListUsersHook>>>
>;
export type ListUsersQueryError = unknown;
/**
* @summary List all users
*/
export function useListUsers<
TData = Awaited<ReturnType<ReturnType<typeof useListUsersHook>>>,
TError = unknown,
>(
params?: ListUsersParams,
options?: {
query?: UseQueryOptions<
Awaited<ReturnType<ReturnType<typeof useListUsersHook>>>,
TError,
TData
>;
}
): UseQueryResult<TData, TError> & { queryKey: QueryKey } {
const queryOptions = useListUsersQueryOptions(params, options);
const query = useQuery(queryOptions) as UseQueryResult<TData, TError> & {
queryKey: QueryKey;
};
query.queryKey = queryOptions.queryKey;
return query;
}
From this, we can see that useCustomInstance
is imported and queryFn is defined from there.
What I was curious about was how the path and name specified in override.mutator in orval.config.js were included in the output of code generation. Let's see it.
Most of the process can be confirmed by following the generateMutator
function.
Even though it's specified in the override.mutator in orval.config.js, we just need to check the file.
export const generateMutator = async ({
...
}: {
...
}): Promise<GeneratorMutator | undefined> => {
if (!mutator || !output) {
return;
}
const isDefault = mutator.default;
const importName = mutator.name ? mutator.name : `${name}Mutator`;
const importPath = mutator.path;
...
const { file, cached } = await loadFile<string>(importPath, {
...
});
In this case, esbuild is used to bundle the file containing the mutator if it's a ts or mjs file.
The bundled result looks something like this, although some parts are omitted.
If you want to check it on your own, please visit ast-explorer (make sure to set the language to JavaScript and the parser to acorn).
var __create = Object.create;
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/lib/axios.ts
var axios_exports = {};
__export(axios_exports, {
AXIOS_INSTANCE: () => AXIOS_INSTANCE,
useCustomInstance: () => useCustomInstance
});
module.exports = __toCommonJS(axios_exports);
var import_axios = __toESM(require("axios"), 1);
var AXIOS_INSTANCE = import_axios.default.create({
baseURL: ""
});
var useCustomInstance = () => {
return (config) => {
const source = import_axios.default.CancelToken.source();
const promise = AXIOS_INSTANCE(__spreadProps(__spreadValues({}, config), {
headers: {
Authorization: `Bearer YOUR_TOKEN_HERE`
},
cancelToken: source.token
})).then(({ data }) => data);
promise.cancel = () => {
source.cancel("Query was cancelled by React Query");
};
return promise;
};
};
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
AXIOS_INSTANCE,
useCustomInstance
});
Parse the file and the mutator function to identify the mutator. In this case, the node of the mutator is identified as follows.
If it's confirmed to actually exist, it can be included as outputFile's content.
const parseFunction = (
ast: any,
name: string,
): GeneratorMutatorParsingInfo | undefined => {
const node = ast?.body?.find((childNode: any) => {
if (childNode.type === 'VariableDeclaration') {
return childNode.declarations.find((d: any) => d.id.name === name);
}
// omitted
};
If the mutator does not exist, an error is output and the process ends.
if (!mutatorInfo) {
createLogger().error(
chalk.red(
`Your mutator file doesn't have the ${mutatorInfoName} exported function`
)
);
process.exit(1);
}