Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions docs/content/docs/guides/react-query.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,61 @@ export default defineConfig({
});
```

## Axios Instance Injection

Use `httpClientInjection: 'reactQueryMeta'` to inject a configured axios instance at runtime via the QueryClient. This is useful when publishing generated code as an NPM package — consumers provide their own axios with interceptors, auth, and base URL.

In your `orval.config.ts`:

```ts
import { defineConfig } from 'orval';

export default defineConfig({
petstore: {
output: {
client: 'react-query',
httpClient: 'axios',
httpClientInjection: 'reactQueryMeta',
},
input: {
target: './petstore.yaml',
},
},
});
```

In your app entry point:

```tsx
import axios from 'axios';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useListPets } from './api/endpoints/petstore';

const axiosInstance = axios.create({ baseURL: 'https://api.example.com' });

const queryClient = new QueryClient({
defaultOptions: {
queries: { meta: { axiosInstance } },
mutations: { meta: { axiosInstance } },
},
});

function Pets() {
const { data } = useListPets();
return <ul>{data?.map((pet) => <li key={pet.id}>{pet.name}</li>)}</ul>;
}

function App() {
return (
<QueryClientProvider client={queryClient}>
<Pets />
</QueryClientProvider>
);
}
```

The generated hooks are SSR-compatible — prefetch works out of the box with `dehydrate()`. See the [complete example](https://github.com/orval-labs/orval/blob/master/samples/react-query/queryclient-meta).

## Full Example

See the [complete React Query example](https://github.com/orval-labs/orval/blob/master/samples/react-query/basic) on GitHub.
34 changes: 34 additions & 0 deletions docs/content/docs/reference/configuration/output.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,40 @@ export default defineConfig({
});
```

## httpClientInjection

**Type:** `'none' | 'reactQueryMeta'`
**Default:** `'none'`

Controls how the HTTP client instance is provided to generated hooks. When set to `reactQueryMeta`, the generated hooks read the axios instance from `QueryClient.defaultOptions.meta.axiosInstance` at runtime, enabling SDK-style distribution where consumers inject their own configured instance.

```ts
export default defineConfig({
petstore: {
output: {
client: 'react-query',
httpClient: 'axios',
httpClientInjection: 'reactQueryMeta',
},
},
});
```

Consumer setup:

```ts
const axiosInstance = axios.create({ baseURL: 'https://api.example.com' });

const queryClient = new QueryClient({
defaultOptions: {
queries: { meta: { axiosInstance } },
mutations: { meta: { axiosInstance } },
},
});
```

The generated hooks automatically resolve the instance via `useQueryClient()` inside `queryFn`/`mutationFn`. Falls back to default axios if not configured. Incompatible with custom `mutator`.

## schemas

**Type:** `String`
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export type NormalizedOutputOptions = {
override: NormalizedOverrideOutput;
client: OutputClient | OutputClientFunc;
httpClient: OutputHttpClient;
httpClientInjection: OutputHttpClientInjection;
clean: boolean | string[];
docs: boolean | OutputDocsOptions;
prettier: boolean;
Expand Down Expand Up @@ -235,6 +236,7 @@ export type OutputOptions = {
override?: OverrideOutput;
client?: OutputClient | OutputClientFunc;
httpClient?: OutputHttpClient;
httpClientInjection?: OutputHttpClientInjection;
clean?: boolean | string[];
docs?: boolean | OutputDocsOptions;
prettier?: boolean;
Expand Down Expand Up @@ -297,6 +299,14 @@ export const OutputHttpClient = {
export type OutputHttpClient =
(typeof OutputHttpClient)[keyof typeof OutputHttpClient];

export const OutputHttpClientInjection = {
NONE: 'none',
REACT_QUERY_META: 'reactQueryMeta',
} as const;

export type OutputHttpClientInjection =
(typeof OutputHttpClientInjection)[keyof typeof OutputHttpClientInjection];

export const OutputMode = {
SINGLE: 'single',
SPLIT: 'split',
Expand Down Expand Up @@ -771,6 +781,7 @@ export interface GlobalOptions {
mock?: boolean | GlobalMockOptions;
client?: OutputClient;
httpClient?: OutputHttpClient;
httpClientInjection?: OutputHttpClientInjection;
mode?: OutputMode;
tsconfig?: string | Tsconfig;
packageJson?: string;
Expand Down Expand Up @@ -1002,6 +1013,7 @@ export type ClientDependenciesBuilder = (
httpClient?: OutputHttpClient,
hasTagsMutator?: boolean,
override?: NormalizedOverrideOutput,
httpClientInjection?: OutputHttpClientInjection,
) => GeneratorDependency[];

export type ClientMockGeneratorImplementation = {
Expand Down
1 change: 1 addition & 0 deletions packages/orval/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export const generateClientImports: GeneratorClientImports = ({
output.httpClient,
hasTagsMutator,
output.override,
output.httpClientInjection,
),
...imports,
]
Expand Down
21 changes: 20 additions & 1 deletion packages/orval/src/utils/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
type OptionsExport,
OutputClient,
OutputHttpClient,
OutputHttpClientInjection,
OutputMode,
type OverrideOutput,
PropertySortOrder,
Expand Down Expand Up @@ -132,7 +133,7 @@ export async function normalizeOptions(
workspace,
);

const { clean, prettier, client, httpClient, mode, biome } = globalOptions;
const { clean, prettier, client, httpClient, httpClientInjection, mode, biome } = globalOptions;

const tsconfig = await loadTsconfig(
outputOptions.tsconfig || globalOptions.tsconfig,
Expand Down Expand Up @@ -206,6 +207,10 @@ export async function normalizeOptions(
((outputOptions.client ?? client) === OutputClient.ANGULAR_QUERY
? OutputHttpClient.ANGULAR
: OutputHttpClient.FETCH),
httpClientInjection:
outputOptions.httpClientInjection ??
httpClientInjection ??
OutputHttpClientInjection.NONE,
mode: normalizeOutputMode(outputOptions.mode ?? mode),
mock,
clean: outputOptions.clean ?? clean ?? false,
Expand Down Expand Up @@ -405,6 +410,20 @@ export async function normalizeOptions(
throw new Error(chalk.red(`Config require an output target or schemas`));
}

// Validate that httpClientInjection: 'reactQueryMeta' is not used with custom mutator
if (
normalizedOptions.output.httpClientInjection ===
OutputHttpClientInjection.REACT_QUERY_META &&
normalizedOptions.output.override?.mutator
) {
throw new Error(
chalk.red(
`httpClientInjection: "reactQueryMeta" is incompatible with custom mutator. ` +
`The mutator handles the HTTP client internally, so axios injection via QueryClient meta is not supported.`,
),
);
}

return normalizedOptions;
}

Expand Down
50 changes: 44 additions & 6 deletions packages/query/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type GetterResponse,
isSyntheticDefaultImportsAllow,
OutputHttpClient,
OutputHttpClientInjection,
pascal,
toObjectString,
} from '@orval/core';
Expand Down Expand Up @@ -43,6 +44,24 @@ export const AXIOS_DEPENDENCIES: GeneratorDependency[] = [
},
];

export const AXIOS_DEPENDENCIES_WITH_INSTANCE: GeneratorDependency[] = [
{
exports: [
{
name: 'axios',
default: true,
values: true,
syntheticDefaultImport: true,
},
{ name: 'AxiosInstance' },
{ name: 'AxiosRequestConfig' },
{ name: 'AxiosResponse' },
{ name: 'AxiosError' },
],
dependency: 'axios',
},
];

export const ANGULAR_HTTP_DEPENDENCIES: GeneratorDependency[] = [
{
exports: [
Expand Down Expand Up @@ -384,6 +403,10 @@ export const generateAxiosRequestFunction = (
context.output.tsconfig,
);

const isReactQueryMeta =
context.output.httpClientInjection ===
OutputHttpClientInjection.REACT_QUERY_META;

const options = generateOptions({
route,
body,
Expand All @@ -410,14 +433,29 @@ export const generateAxiosRequestFunction = (

const queryProps = toObjectString(props, 'implementation');

const httpRequestFunctionImplementation = `${override.query.shouldExportHttpClient ? 'export ' : ''}const ${operationName} = (\n ${queryProps} ${optionsArgs} ): Promise<AxiosResponse<${
response.definition.success || 'unknown'
}>> => {
// For reactQueryMeta mode, add axiosInstance as first parameter
const axiosInstanceParam = isReactQueryMeta
? 'axiosInstance: AxiosInstance,\n '
: '';

// Use axiosInstance if provided, otherwise use global axios
const axiosRef = isReactQueryMeta
? 'axiosInstance'
: `axios${isSyntheticDefaultImportsAllowed ? '' : '.default'}`;

// For reactQueryMeta mode, return response.data (serializable for SSR)
// instead of the full AxiosResponse (which contains non-serializable functions)
const returnType = isReactQueryMeta
? response.definition.success || 'unknown'
: `AxiosResponse<${response.definition.success || 'unknown'}>`;
const returnStatement = isReactQueryMeta
? `${axiosRef}.${verb}(${options}).then(res => res.data)`
: `${axiosRef}.${verb}(${options})`;

const httpRequestFunctionImplementation = `${override.query.shouldExportHttpClient ? 'export ' : ''}const ${operationName} = (\n ${axiosInstanceParam}${queryProps} ${optionsArgs} ): Promise<${returnType}> => {
${isVue ? vueUnRefParams(props) : ''}
${bodyForm}
return axios${
isSyntheticDefaultImportsAllowed ? '' : '.default'
}.${verb}(${options});
return ${returnStatement};
}
`;

Expand Down
74 changes: 74 additions & 0 deletions packages/query/src/dependencies.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { OutputHttpClient, OutputHttpClientInjection } from '@orval/core';
import { describe, expect, it } from 'vitest';

import { getReactQueryDependencies } from './dependencies';

describe('getReactQueryDependencies with httpClientInjection', () => {
const packageJson = {
dependencies: {
'@tanstack/react-query': '^5.0.0',
},
};

it('should include AxiosInstance when reactQueryMeta is enabled', () => {
const deps = getReactQueryDependencies(
false,
false,
packageJson,
OutputHttpClient.AXIOS,
false,
undefined,
OutputHttpClientInjection.REACT_QUERY_META,
);

const axiosDep = deps.find((d) => d.dependency === 'axios');
const axiosExports = axiosDep?.exports.map((e) => e.name) ?? [];
expect(axiosExports).toContain('AxiosInstance');
});

it('should not include AxiosInstance when injection is none', () => {
const deps = getReactQueryDependencies(
false,
false,
packageJson,
OutputHttpClient.AXIOS,
false,
undefined,
OutputHttpClientInjection.NONE,
);

const axiosDep = deps.find((d) => d.dependency === 'axios');
const axiosExports = axiosDep?.exports.map((e) => e.name) ?? [];
expect(axiosExports).not.toContain('AxiosInstance');
});

it('should not include AxiosInstance when httpClient is fetch', () => {
const deps = getReactQueryDependencies(
false,
false,
packageJson,
OutputHttpClient.FETCH,
false,
undefined,
OutputHttpClientInjection.REACT_QUERY_META,
);

const axiosDep = deps.find((d) => d.dependency === 'axios');
expect(axiosDep).toBeUndefined();
});

it('should not include axios deps when hasGlobalMutator', () => {
const deps = getReactQueryDependencies(
true,
false,
packageJson,
OutputHttpClient.AXIOS,
false,
undefined,
OutputHttpClientInjection.REACT_QUERY_META,
);

const axiosDep = deps.find((d) => d.dependency === 'axios');
expect(axiosDep).toBeUndefined();
});
});
Loading
Loading