orval
orval copied to clipboard
support `fetch` client
I'm opening this issue to clarify the support status of the fetch client.
Description
I added a new fetch client.
Becouse, since the fetch API is mature, it has the advantage of reducing the bundle size of the application compared to using Axios, and I feel that this is sufficient in some cases.
Furthermore, we think it can be used to select an HTTP client with tanstack query or swr, or when calling an API from a server-side framework.
Ref: https://developer.mozilla.org/en-US/docs/Web/API/fetch
Todo
- [x] add a minimal
fetchclient usingfetchAPI. - [x] add a sample application using the
fetchclient withNext.js. - [x] add tests for the
fetchclient. - [x] add a document for the
fetchclient. - [x] add other enhancements. For example, customization by specifying mutator.
- [ ] support
fetchashttp fetcherin theswrclient. - [ ] support
fetchashttp fetcherin thequeryclient.
I think the main benefit of using Axios in this context is the easy creation of a custom instance with interceptors. The most common use case is adding an Auth header in a request interceptor. Do you see this as possible with the Fetch API? If so, could you provide an example as part of this effort? If that is a no-brainer, I would happily replace Axios with Fetch
UPDATE Only after I wrote this did I see you already had a task there: "add other enhancements, for example, customization by specifying mutator." But it would be great to specifically demonstrate an async request interceptor
@oferitz
Thank you for give feed back π
I agree that Axios interceptor is useful.γBut I didn't think about that for now.
However, I like your idea and will think about how to use that use0case, such as including a sample app or guide.
Axios doesn't run on winter runtimes like Cloudflare, Vercel Edge and Deno. This is why the fetch adapter would be super useful.
Neat! Thank you @soartec-lab for your implementation. We've already switched to this instead of our own fetchClient.ts via a mutator override.
I've a question regarding handling different baseUrl's for multiple environments (local, test, prod, etc):
- We've made a custom wrapper to handle authentication serverside, it has the following structure
withAuth(fn, fnParams, fetchOverrideParams). - In
orval.config.tswe've added the following:baseUrl: `$\{process.env.API_URL}`,. This works because the value is just being concatenated during code generation. See the Next.js example, here the value would bereturn `${process.env.API_URL}/pets`;. - This is so that we can keep using the benefit of having the env variable be evaluated at runtime on the server, instead of on build time.
But this probably isn't the intended way to implement this, do you have any suggestions regarding this?
Hi, @n2k3 Thanks for your comment.
baseUrl:
$\{process.env.API_URL},
That's certainly works. And as far as I know, this is currently the only workaround for switching variables for each environment. This issue is independent of the fetch client, but other clients have similar problems, so I'll look into it.
@soartec-lab we could have a standalone object that could be overridable like axios is doing. Like here https://gist.github.com/anymaniax/44c1331a5643081a82da070e45f405f0#file-http-instance-ts-L34
It`s great, I would like to implement this when creating a custom instance π
Hey, @anymaniax
Could you please tell me how to use the HTTP_INSTANCE defined in the fetch sample you gave?
https://gist.github.com/anymaniax/44c1331a5643081a82da070e45f405f0#file-http-instance-ts-L34
I would like to implement something like an Axios interceptor as a sample.
Is this defined as a global variable? I would like to refer to your usage.
@oferitz @n2k3
The fetch client now supports custom mutators via #1457 π
As a result, it is now possible to intercept requests and responses, as well as dynamically transform base URL.
https://github.com/anymaniax/orval/blob/master/samples/next-app-with-fetch/custom-fetch.ts#L16-L29
I'm still looking for feedback, so please let me know if you find anything.
@soartec-lab Fantastic work overall. However, I encountered a problem while trying to use your custom fetch with react-query as the client. The generated hooks have a TypeScript error:
Argument of type { url: string; method: string; } is not assignable to parameter of type string
Here is a generated hook with the above error, for example:
export const createNewTodo = (
options?: SecondParameter<typeof customFetch>
) => {
return customFetch<ApiResponse>(
{ url: `/v1/todo`, method: 'POST' },
options
);
};
So the function expecting url and method as object in the 1st argument but we only pass url because method is already part of the 2nd argument options. can you advise how to fix it?
OK, I'll check it later.
@oferitz
Could you please explain in detail how you are using it and the actual code?
Are you passing the client generated with fetch client as an argument as the react-query http fetcher?
@soartec-lab using your custom fetch like here: https://github.com/anymaniax/orval/blob/master/samples/next-app-with-fetch/custom-fetch.ts#L16-L29 And this is the Orval config:
output: {
target: 'src/generated',
schemas: 'src/generated',
client: 'react-query',
mode: 'tags-split',
mock: false,
biome: true,
clean: true,
override: {
mutator: {
path: 'src/lib/api-client.ts',
name: 'customFetch'
}
}
}
And this is the generated file with the error:
/**
* Generated by orval v6.30.2 πΊ
* Do not edit manually.
* My Api
* OpenAPI spec version: v1
*/
import { useMutation } from '@tanstack/react-query'
import type {
MutationFunction,
UseMutationOptions,
UseMutationResult
} from '@tanstack/react-query'
import { customFetch } from '../../../lib/api-client'
import type { ApiResponse } from '.././'
type SecondParameter<T extends (...args: any) => any> = Parameters<T>[1]
export const createNewTodo= (
options?: SecondParameter<typeof customFetch>
) => {
return customFetch<ApiResponse>(
{ url: `/v1/todo`, method: 'POST' },
options
)
}
export const getCreateNewTodoMutationOptions = <
TError = unknown,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createNewTodo>>,
TError,
void,
TContext
>
request?: SecondParameter<typeof customFetch>
}): UseMutationOptions<
Awaited<ReturnType<typeof createNewTodo>>,
TError,
void,
TContext
> => {
const { mutation: mutationOptions, request: requestOptions } = options ?? {}
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createNewTodo>>,
void
> = () => {
return createNewTodo(requestOptions)
}
return { mutationFn, ...mutationOptions }
}
export type CreateNewTodoMutationResult = NonNullable<
Awaited<ReturnType<typeof createNewTodo>>
>
export type CreateNewTodoMutationError = unknown
export const useCreateNewTodo = <
TError = unknown,
TContext = unknown
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createNewTodo>>,
TError,
void,
TContext
>
request?: SecondParameter<typeof customFetch>
}): UseMutationResult<
Awaited<ReturnType<typeof createNewTodo>>,
TError,
void,
TContext
> => {
const mutationOptions = getCreateNewTodoMutationOptions(options)
return useMutation(mutationOptions)
}
@oferitz
It became clear. There is an explanation below about how to use the fetch client with react-query, but I have not used it yet.
https://orval.dev/guides/custom-client
As part of this support, we plan to enhance it so that you can use the fetch client of react-query, so could you please wait until then?
Also, since it has nothing to do with adding support for the fetch client, please open it as a separate issue. This way, you can receive support from others.
@soartec-lab https://github.com/anymaniax/orval/issues/1481 Thanks.
As part of this support, we plan to enhance it so that you can use the fetch client of react-query, so could you please wait until then?
excited for this! although I'm using vue-query, hopefully it will get fetch support as well.
@soartec-lab Why are Content-Types not output in the new fetch client? Or am I missing something? Orval Config
import { defineConfig } from "orval";
export default defineConfig({
onblok: {
input: {
target: "",
},
output: {
mock: false,
prettier: true,
target: "./app/_services/index.ts",
client: "fetch",
mode: "single",
override: {
mutator: {
path: "./app/_services/custom-fetch.ts",
name: "customFetch",
},
},
},
},
});
Custom Fetch
const getBody = <T>(c: Response | Request): Promise<T> => {
const contentType = c.headers.get("content-type");
if (contentType && contentType.includes("application/json")) {
return c.json();
}
if (contentType && contentType.includes("application/pdf")) {
return c.blob() as Promise<T>;
}
return c.text() as Promise<T>;
};
// NOTE: Update just base url
const getUrl = (contextUrl: string): string => {
const baseUrl = process.env.NEXT_PUBLIC_API_URL;
const url = new URL(`${baseUrl}${contextUrl}`);
const pathname = url.pathname;
const search = url.search;
const requestUrl = new URL(`${baseUrl}${pathname}${search}`);
return requestUrl.toString();
};
// NOTE: Add headers
const getHeaders = (headers?: HeadersInit): HeadersInit => {
return {
"X-Channel": "WEB",
...headers,
};
};
export const customFetch = async <T>(
url: string,
options: RequestInit,
): Promise<T> => {
const requestUrl = getUrl(url);
const requestHeaders = getHeaders(options.headers);
const requestInit: RequestInit = {
...options,
headers: requestHeaders,
};
const request = new Request(requestUrl, requestInit);
const response = await fetch(request);
const data = (await getBody<T>(response)) as any;
if (!response.ok) {
throw data?.error;
}
return { status: response.status, data: data?.data } as T;
};
Additionally, if there is no parameter, it seems necessary to check the size in getUrl and remove the question mark. Original
export const getCategoryZeroLevelUrl = (
categoryLevel0: string,
params?: CategoryZeroLevelParams,
) => {
const normalizedParams = new URLSearchParams();
normalizedParams.size
Object.entries(params || {}).forEach(([key, value]) => {
if (value === null) {
normalizedParams.append(key, "null");
} else if (value !== undefined) {
normalizedParams.append(key, value.toString());
}
});
return `/categories/by-path/${categoryLevel0}?${normalizedParams.toString()}`;
};
Fixed
export const getCategoryZeroLevelUrl = (
categoryLevel0: string,
params?: CategoryZeroLevelParams,
) => {
const normalizedParams = new URLSearchParams();
normalizedParams.size
Object.entries(params || {}).forEach(([key, value]) => {
if (value === null) {
normalizedParams.append(key, "null");
} else if (value !== undefined) {
normalizedParams.append(key, value.toString());
}
});
if(normalizedParams.size === 0){
return `/categories/by-path/${categoryLevel0}`;
}
return `/categories/by-path/${categoryLevel0}?${normalizedParams.toString()}`;
};
@hasanaktas thank you for let me know π Could you please make a new issue for each of these issues with reproduce?
@soartec-lab
I sent a pull request but I couldn't find a solution for response-type
https://github.com/anymaniax/orval/pull/1504
@oferitz @davidysoards
Hi, there.
In the next version v6.32.0, you will be able to use fetch as an http client with react-query, vue-query, and svelte-query. If you are interested, please check the next version after release.
https://orval.dev/reference/configuration/output#httpclient
wonderful! thank you for all your hard work on this feature @soartec-lab π
@soartec-lab have you set a release date for version 6.32.0?
Thank you for this feature - it will help me a lot!
@xmd5a i just pinged @anymaniax about doing a 6.32.0 release.
Thanks for the great work on the fetch client - I noticed that formdata openapi specifications do not generate as expected:
export const createTranslation = async (createTranslationBody: CreateTranslationBody, options?: RequestInit): Promise<createTranslationResponse> => {
return customFetch<Promise<createTranslationResponse>>(getCreateTranslationUrl(),
{
...options,
method: 'POST',
body: JSON.stringify(
formData,)
}
);}
The formData does not exist in this case and as such this generated code will not work. Is this a known limitation at the moment?
@jamesleeht
Thank you for let me know. I knew that, but i forget fix it. So, i'll fix that π
@soartec-lab you are amazing - thanks for such a quick fix! sent you some beer money πΊ
@jamesleeht Thank you for made this report as well. Let's looking forward the next release together. And from now on too π»
7.0 is released lets close this ticket and let people open new individual bug tickets for fetch against 7.0
7.0 is released lets close this ticket and let people open new individual bug tickets for fetch against 7.0
π curious why this is a major release tho? no breaking changes are listed.
Because we had a lot of new feature and refactor lately. And we will completely remove some old api already mark as legacy. Also we moved the repository to an organization so itβs also to start a new era for the project