Skip to content

Commit cf1aaf9

Browse files
committed
feat(react-query): add httpClientInjection: 'reactQueryMeta' for axios instance injection
Adds support for injecting axios instances at runtime via QueryClient.defaultOptions.meta, enabling NPM package distribution, SSR, and mock injection without custom mutators. Generated hooks automatically resolve the axios instance from: - queries.meta.axiosInstance for queries - mutations.meta.axiosInstance for mutations HTTP functions now return Promise<T> (data unwrapped) instead of Promise<AxiosResponse<T>> to ensure SSR dehydrate() can serialize the cache.
1 parent f9f1659 commit cf1aaf9

File tree

28 files changed

+1192
-42
lines changed

28 files changed

+1192
-42
lines changed

docs/content/docs/guides/react-query.mdx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,61 @@ export default defineConfig({
128128
});
129129
```
130130

131+
## Axios Instance Injection
132+
133+
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.
134+
135+
In your `orval.config.ts`:
136+
137+
```ts
138+
import { defineConfig } from 'orval';
139+
140+
export default defineConfig({
141+
petstore: {
142+
output: {
143+
client: 'react-query',
144+
httpClient: 'axios',
145+
httpClientInjection: 'reactQueryMeta',
146+
},
147+
input: {
148+
target: './petstore.yaml',
149+
},
150+
},
151+
});
152+
```
153+
154+
In your app entry point:
155+
156+
```tsx
157+
import axios from 'axios';
158+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
159+
import { useListPets } from './api/endpoints/petstore';
160+
161+
const axiosInstance = axios.create({ baseURL: 'https://api.example.com' });
162+
163+
const queryClient = new QueryClient({
164+
defaultOptions: {
165+
queries: { meta: { axiosInstance } },
166+
mutations: { meta: { axiosInstance } },
167+
},
168+
});
169+
170+
function Pets() {
171+
const { data } = useListPets();
172+
return <ul>{data?.map((pet) => <li key={pet.id}>{pet.name}</li>)}</ul>;
173+
}
174+
175+
function App() {
176+
return (
177+
<QueryClientProvider client={queryClient}>
178+
<Pets />
179+
</QueryClientProvider>
180+
);
181+
}
182+
```
183+
184+
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).
185+
131186
## Full Example
132187

133188
See the [complete React Query example](https://github.com/orval-labs/orval/blob/master/samples/react-query/basic) on GitHub.

docs/content/docs/reference/configuration/output.mdx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,40 @@ export default defineConfig({
7777
});
7878
```
7979

80+
## httpClientInjection
81+
82+
**Type:** `'none' | 'reactQueryMeta'`
83+
**Default:** `'none'`
84+
85+
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.
86+
87+
```ts
88+
export default defineConfig({
89+
petstore: {
90+
output: {
91+
client: 'react-query',
92+
httpClient: 'axios',
93+
httpClientInjection: 'reactQueryMeta',
94+
},
95+
},
96+
});
97+
```
98+
99+
Consumer setup:
100+
101+
```ts
102+
const axiosInstance = axios.create({ baseURL: 'https://api.example.com' });
103+
104+
const queryClient = new QueryClient({
105+
defaultOptions: {
106+
queries: { meta: { axiosInstance } },
107+
mutations: { meta: { axiosInstance } },
108+
},
109+
});
110+
```
111+
112+
The generated hooks automatically resolve the instance via `useQueryClient()` inside `queryFn`/`mutationFn`. Falls back to default axios if not configured. Incompatible with custom `mutator`.
113+
80114
## schemas
81115

82116
**Type:** `String`

packages/core/src/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export type NormalizedOutputOptions = {
3636
override: NormalizedOverrideOutput;
3737
client: OutputClient | OutputClientFunc;
3838
httpClient: OutputHttpClient;
39+
httpClientInjection: OutputHttpClientInjection;
3940
clean: boolean | string[];
4041
docs: boolean | OutputDocsOptions;
4142
prettier: boolean;
@@ -235,6 +236,7 @@ export type OutputOptions = {
235236
override?: OverrideOutput;
236237
client?: OutputClient | OutputClientFunc;
237238
httpClient?: OutputHttpClient;
239+
httpClientInjection?: OutputHttpClientInjection;
238240
clean?: boolean | string[];
239241
docs?: boolean | OutputDocsOptions;
240242
prettier?: boolean;
@@ -297,6 +299,14 @@ export const OutputHttpClient = {
297299
export type OutputHttpClient =
298300
(typeof OutputHttpClient)[keyof typeof OutputHttpClient];
299301

302+
export const OutputHttpClientInjection = {
303+
NONE: 'none',
304+
REACT_QUERY_META: 'reactQueryMeta',
305+
} as const;
306+
307+
export type OutputHttpClientInjection =
308+
(typeof OutputHttpClientInjection)[keyof typeof OutputHttpClientInjection];
309+
300310
export const OutputMode = {
301311
SINGLE: 'single',
302312
SPLIT: 'split',
@@ -771,6 +781,7 @@ export interface GlobalOptions {
771781
mock?: boolean | GlobalMockOptions;
772782
client?: OutputClient;
773783
httpClient?: OutputHttpClient;
784+
httpClientInjection?: OutputHttpClientInjection;
774785
mode?: OutputMode;
775786
tsconfig?: string | Tsconfig;
776787
packageJson?: string;
@@ -1002,6 +1013,7 @@ export type ClientDependenciesBuilder = (
10021013
httpClient?: OutputHttpClient,
10031014
hasTagsMutator?: boolean,
10041015
override?: NormalizedOverrideOutput,
1016+
httpClientInjection?: OutputHttpClientInjection,
10051017
) => GeneratorDependency[];
10061018

10071019
export type ClientMockGeneratorImplementation = {

packages/orval/src/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export const generateClientImports: GeneratorClientImports = ({
8989
output.httpClient,
9090
hasTagsMutator,
9191
output.override,
92+
output.httpClientInjection,
9293
),
9394
...imports,
9495
]

packages/orval/src/utils/options.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
type OptionsExport,
3333
OutputClient,
3434
OutputHttpClient,
35+
OutputHttpClientInjection,
3536
OutputMode,
3637
type OverrideOutput,
3738
PropertySortOrder,
@@ -132,7 +133,7 @@ export async function normalizeOptions(
132133
workspace,
133134
);
134135

135-
const { clean, prettier, client, httpClient, mode, biome } = globalOptions;
136+
const { clean, prettier, client, httpClient, httpClientInjection, mode, biome } = globalOptions;
136137

137138
const tsconfig = await loadTsconfig(
138139
outputOptions.tsconfig || globalOptions.tsconfig,
@@ -206,6 +207,10 @@ export async function normalizeOptions(
206207
((outputOptions.client ?? client) === OutputClient.ANGULAR_QUERY
207208
? OutputHttpClient.ANGULAR
208209
: OutputHttpClient.FETCH),
210+
httpClientInjection:
211+
outputOptions.httpClientInjection ??
212+
httpClientInjection ??
213+
OutputHttpClientInjection.NONE,
209214
mode: normalizeOutputMode(outputOptions.mode ?? mode),
210215
mock,
211216
clean: outputOptions.clean ?? clean ?? false,
@@ -405,6 +410,20 @@ export async function normalizeOptions(
405410
throw new Error(chalk.red(`Config require an output target or schemas`));
406411
}
407412

413+
// Validate that httpClientInjection: 'reactQueryMeta' is not used with custom mutator
414+
if (
415+
normalizedOptions.output.httpClientInjection ===
416+
OutputHttpClientInjection.REACT_QUERY_META &&
417+
normalizedOptions.output.override?.mutator
418+
) {
419+
throw new Error(
420+
chalk.red(
421+
`httpClientInjection: "reactQueryMeta" is incompatible with custom mutator. ` +
422+
`The mutator handles the HTTP client internally, so axios injection via QueryClient meta is not supported.`,
423+
),
424+
);
425+
}
426+
408427
return normalizedOptions;
409428
}
410429

packages/query/src/client.ts

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
type GetterResponse,
1212
isSyntheticDefaultImportsAllow,
1313
OutputHttpClient,
14+
OutputHttpClientInjection,
1415
pascal,
1516
toObjectString,
1617
} from '@orval/core';
@@ -43,6 +44,24 @@ export const AXIOS_DEPENDENCIES: GeneratorDependency[] = [
4344
},
4445
];
4546

47+
export const AXIOS_DEPENDENCIES_WITH_INSTANCE: GeneratorDependency[] = [
48+
{
49+
exports: [
50+
{
51+
name: 'axios',
52+
default: true,
53+
values: true,
54+
syntheticDefaultImport: true,
55+
},
56+
{ name: 'AxiosInstance' },
57+
{ name: 'AxiosRequestConfig' },
58+
{ name: 'AxiosResponse' },
59+
{ name: 'AxiosError' },
60+
],
61+
dependency: 'axios',
62+
},
63+
];
64+
4665
export const ANGULAR_HTTP_DEPENDENCIES: GeneratorDependency[] = [
4766
{
4867
exports: [
@@ -384,6 +403,10 @@ export const generateAxiosRequestFunction = (
384403
context.output.tsconfig,
385404
);
386405

406+
const isReactQueryMeta =
407+
context.output.httpClientInjection ===
408+
OutputHttpClientInjection.REACT_QUERY_META;
409+
387410
const options = generateOptions({
388411
route,
389412
body,
@@ -410,14 +433,29 @@ export const generateAxiosRequestFunction = (
410433

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

413-
const httpRequestFunctionImplementation = `${override.query.shouldExportHttpClient ? 'export ' : ''}const ${operationName} = (\n ${queryProps} ${optionsArgs} ): Promise<AxiosResponse<${
414-
response.definition.success || 'unknown'
415-
}>> => {
436+
// For reactQueryMeta mode, add axiosInstance as first parameter
437+
const axiosInstanceParam = isReactQueryMeta
438+
? 'axiosInstance: AxiosInstance,\n '
439+
: '';
440+
441+
// Use axiosInstance if provided, otherwise use global axios
442+
const axiosRef = isReactQueryMeta
443+
? 'axiosInstance'
444+
: `axios${isSyntheticDefaultImportsAllowed ? '' : '.default'}`;
445+
446+
// For reactQueryMeta mode, return response.data (serializable for SSR)
447+
// instead of the full AxiosResponse (which contains non-serializable functions)
448+
const returnType = isReactQueryMeta
449+
? response.definition.success || 'unknown'
450+
: `AxiosResponse<${response.definition.success || 'unknown'}>`;
451+
const returnStatement = isReactQueryMeta
452+
? `${axiosRef}.${verb}(${options}).then(res => res.data)`
453+
: `${axiosRef}.${verb}(${options})`;
454+
455+
const httpRequestFunctionImplementation = `${override.query.shouldExportHttpClient ? 'export ' : ''}const ${operationName} = (\n ${axiosInstanceParam}${queryProps} ${optionsArgs} ): Promise<${returnType}> => {
416456
${isVue ? vueUnRefParams(props) : ''}
417457
${bodyForm}
418-
return axios${
419-
isSyntheticDefaultImportsAllowed ? '' : '.default'
420-
}.${verb}(${options});
458+
return ${returnStatement};
421459
}
422460
`;
423461

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { OutputHttpClient, OutputHttpClientInjection } from '@orval/core';
2+
import { describe, expect, it } from 'vitest';
3+
4+
import { getReactQueryDependencies } from './dependencies';
5+
6+
describe('getReactQueryDependencies with httpClientInjection', () => {
7+
const packageJson = {
8+
dependencies: {
9+
'@tanstack/react-query': '^5.0.0',
10+
},
11+
};
12+
13+
it('should include AxiosInstance when reactQueryMeta is enabled', () => {
14+
const deps = getReactQueryDependencies(
15+
false,
16+
false,
17+
packageJson,
18+
OutputHttpClient.AXIOS,
19+
false,
20+
undefined,
21+
OutputHttpClientInjection.REACT_QUERY_META,
22+
);
23+
24+
const axiosDep = deps.find((d) => d.dependency === 'axios');
25+
const axiosExports = axiosDep?.exports.map((e) => e.name) ?? [];
26+
expect(axiosExports).toContain('AxiosInstance');
27+
});
28+
29+
it('should not include AxiosInstance when injection is none', () => {
30+
const deps = getReactQueryDependencies(
31+
false,
32+
false,
33+
packageJson,
34+
OutputHttpClient.AXIOS,
35+
false,
36+
undefined,
37+
OutputHttpClientInjection.NONE,
38+
);
39+
40+
const axiosDep = deps.find((d) => d.dependency === 'axios');
41+
const axiosExports = axiosDep?.exports.map((e) => e.name) ?? [];
42+
expect(axiosExports).not.toContain('AxiosInstance');
43+
});
44+
45+
it('should not include AxiosInstance when httpClient is fetch', () => {
46+
const deps = getReactQueryDependencies(
47+
false,
48+
false,
49+
packageJson,
50+
OutputHttpClient.FETCH,
51+
false,
52+
undefined,
53+
OutputHttpClientInjection.REACT_QUERY_META,
54+
);
55+
56+
const axiosDep = deps.find((d) => d.dependency === 'axios');
57+
expect(axiosDep).toBeUndefined();
58+
});
59+
60+
it('should not include axios deps when hasGlobalMutator', () => {
61+
const deps = getReactQueryDependencies(
62+
true,
63+
false,
64+
packageJson,
65+
OutputHttpClient.AXIOS,
66+
false,
67+
undefined,
68+
OutputHttpClientInjection.REACT_QUERY_META,
69+
);
70+
71+
const axiosDep = deps.find((d) => d.dependency === 'axios');
72+
expect(axiosDep).toBeUndefined();
73+
});
74+
});

0 commit comments

Comments
 (0)