orval icon indicating copy to clipboard operation
orval copied to clipboard

support `fetch` client

Open soartec-lab opened this issue 1 year ago β€’ 16 comments

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 fetch client using fetch API.
  • [x] add a sample application using the fetch client with Next.js.
  • [x] add tests for the fetch client.
  • [x] add a document for the fetch client.
  • [x] add other enhancements. For example, customization by specifying mutator.
  • [ ] support fetch as http fetcher in the swr client.
  • [ ] support fetch as http fetcher in the query client.

soartec-lab avatar May 20 '24 09:05 soartec-lab

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 avatar May 30 '24 06:05 oferitz

@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.

soartec-lab avatar May 31 '24 00:05 soartec-lab

Axios doesn't run on winter runtimes like Cloudflare, Vercel Edge and Deno. This is why the fetch adapter would be super useful.

jokull avatar Jun 02 '24 12:06 jokull

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.ts we'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 be return `${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?

n2k3 avatar Jun 12 '24 09:06 n2k3

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 avatar Jun 12 '24 10:06 soartec-lab

@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

anymaniax avatar Jun 12 '24 10:06 anymaniax

It`s great, I would like to implement this when creating a custom instance πŸ‘

soartec-lab avatar Jun 12 '24 11:06 soartec-lab

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.

soartec-lab avatar Jun 17 '24 00:06 soartec-lab

@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 avatar Jun 17 '24 23:06 soartec-lab

@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?

oferitz avatar Jun 24 '24 07:06 oferitz

OK, I'll check it later.

soartec-lab avatar Jun 24 '24 09:06 soartec-lab

@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 avatar Jun 24 '24 11:06 soartec-lab

@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 avatar Jun 24 '24 12:06 oferitz

@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 avatar Jun 24 '24 23:06 soartec-lab

@soartec-lab https://github.com/anymaniax/orval/issues/1481 Thanks.

oferitz avatar Jun 25 '24 06:06 oferitz

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.

davidysoards avatar Jun 30 '24 16:06 davidysoards

@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 avatar Jul 05 '24 12:07 hasanaktas

@hasanaktas thank you for let me know πŸ‘ Could you please make a new issue for each of these issues with reproduce?

soartec-lab avatar Jul 05 '24 13:07 soartec-lab

@soartec-lab

I sent a pull request but I couldn't find a solution for response-type

https://github.com/anymaniax/orval/pull/1504

hasanaktas avatar Jul 05 '24 14:07 hasanaktas

@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

soartec-lab avatar Jul 16 '24 00:07 soartec-lab

wonderful! thank you for all your hard work on this feature @soartec-lab πŸ™

davidysoards avatar Jul 16 '24 03:07 davidysoards

@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 avatar Jul 18 '24 11:07 xmd5a

@xmd5a i just pinged @anymaniax about doing a 6.32.0 release.

melloware avatar Jul 18 '24 11:07 melloware

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 avatar Jul 20 '24 19:07 jamesleeht

@jamesleeht

Thank you for let me know. I knew that, but i forget fix it. So, i'll fix that πŸ‘

soartec-lab avatar Jul 21 '24 00:07 soartec-lab

@soartec-lab you are amazing - thanks for such a quick fix! sent you some beer money 🍺

jamesleeht avatar Jul 21 '24 02:07 jamesleeht

@jamesleeht Thank you for made this report as well. Let's looking forward the next release together. And from now on too 🍻

soartec-lab avatar Jul 21 '24 02:07 soartec-lab

7.0 is released lets close this ticket and let people open new individual bug tickets for fetch against 7.0

melloware avatar Jul 22 '24 14:07 melloware

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.

davidysoards avatar Jul 22 '24 14:07 davidysoards

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

anymaniax avatar Jul 22 '24 15:07 anymaniax