Skip to content

Generates react-query hooks based on Swagger API definitions

License

Notifications You must be signed in to change notification settings

Shaddix/react-query-swagger

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

react-query-swagger

npm version npm MIT Types - TypeScript

This projects autogenerates @tanstack/query hooks or strongly-typed axios/fetch clients based on Swagger API definitions.

Main features

  • Support for DateTime and Date (i.e. you get JS Date objects from HTTP client calls)
  • Everything is treeshakable

So, given that you have a petstore-like API definition, you could autogenerate a list of react-query hooks, to call GET methods from the API (queries). or POST/PUT/PATCH/DELETE methods (mutations).

You could also use this library if you want to generate nice tree-shakable HTTP clients for your Swagger API definition (we use NSwag under the hood).

How to add

Install the package into your project using yarn/npm (as a dev-dependency). You'll also need to add @tanstack/query (which you probably already have if you are interested in this library).

yarn add react-query-swagger

Then create/update your autogenerated hooks by calling (adjusting the URL and output path)

npx react-query-swagger /tanstack /input:https://petstore.swagger.io/v2/swagger.json /output:src/api/axios-client.ts /template:Axios

Other frameworks

  • react-query v3: please REMOVE a /tanstack switch from all commands
  • Vue: please replace a /tanstack switch with /vue
  • Solid: COMING SOON. Please +1 in the feature request if you want it to be available.

This will generate API clients based on Axios. If you prefer fetch, just use it as a template (mind the last parameter)

yarn react-query-swagger /input:https://petstore.swagger.io/v2/swagger.json /output:src/api/axios-client.ts /template:Fetch

You will probably want to add this script to your package.json to call it every time your API changes.

All parameters are passed to NSwag, you could read about them in NSwag documentation. Personally I tend to use it with few additional parameters, which are combined under /use-recommended-configuration:

yarn react-query-swagger /tanstack /input:https://petstore.swagger.io/v2/swagger.json /output:src/api/axios-client.ts /template:Axios /serviceHost:. /use-recommended-configuration

How to use

You could check a pet-client example, which shows the list of pets. It's a standard react-query setup, to query some pet data you just need to write:

const petsQuery = ClientQuery.useFindPetsByStatusQuery([
  Status.Available,
  Status.Pending,
  Status.Sold,
]);
// then just use usual query properties
console.log('isLoading', petsQuery.petsQuery.data?.length);

to perform some mutation you could call

const addPetMutation = ClientQuery.useAddPetMutation();
// and later when submitting the form
addPetMutation.mutate(new Pet({ name: 'blablabla', photoUrls: [] }));

Pass AxiosRequestConfig to a query (for axios users)

You could pass AxiosRequestConfig parameters for each request via the last parameter of useQuery. E.g.:

const petsQuery = ClientQuery.useFindPetsByStatusQuery(
  [Status.Available, Status.Pending, Status.Sold],
  queryParams,
  { timeout: 1000 } /** this param accepts AxiosRequestConfig **/,
);

Configuration

setBaseUrl(baseUrl: string)

Sets base URL for all queries

setAxiosFactory(factory: () => AxiosInstance)

Sets the function which returns Axios instance to be used in http request. By default axios.create() is called for every http request (this method only exists if you generated client using Axios template).

setFetchFactory(factory: () => { fetch(url: RequestInfo, init?: RequestInit): Promise })

Sets the function to return the fetchfunction to be used in http request. By default window is returned, which contains the default fetch function. This method only exists if you used Fetch template.

Configure query options

You could define additional UseQueryOptions for each query by calling set*QueryName*DefaultOptions

AxiosQuery.ClientQuery.setFindPetsByStatusDefaultOptions({
  cacheTime: 10000
});

Configure global Axios options

If you use Axios, you could adjust AxiosRequestConfig per endpoint by using set*QueryName*RequestConfig

AxiosQuery.ClientQuery.setFindPetsByStatusRequestConfig({
  timeout: 10000
});

get*QueryName*RequestConfig and patch*QueryName*RequestConfig are also available.

persistQueryClient support

React-query has an experimental support for persisting and restoring query cache (to preserve the cache between e.g. browser restarts). react-query-swagger requires additional configuration to correctly work with hydration (cache restoration) because of:

  1. All internal DTOs are JS classes, which are not recreated by JSON.parse (which is used by persisters by default).
  2. react-query-swagger has Date objects in DTOs, which are not restored by JSON.parse as well.

So to make them work together correctly, you have to provide a special hydration function (which is autogenerated along with API clients) and call initPersister:

initPersister();
const localStoragePersister = createSyncStoragePersister({
  storage: window.localStorage,

  // You need to import `persisterDeserialize` function from your `api-client.ts` and specify it as a deserialize function.
  deserialize: persisterDeserialize,
});

For useInfiniteQuery the queryKey parameter should start with 2 same items as the underlying 'normal' query (see details).

QueryMetaProvider

Injects meta option to all queries in children components. Might be useful if e.g. you want to refetch all queries in certain part of your app.

First wrap your component in QueryMetaProvider and specify your meta tags (make sure they are constant):

<QueryMetaProvider meta={headerMeta}>
    { /* Your app components (e.g. AppHeader */ }
</QueryMetaProvider>

const headerMeta = { region: 'header' }

You could refetch based on meta via the following call:

queryClient.refetchQueries({ predicate: (query) => ((query as any).observers as QueryObserver[]).find((observer) => observer.options.meta?.region === 'header') })

Additional flags

In addition to NSwag parameters we have 4 specific parameters:

/minimal

It generates Interfaces instead of Classes, which minimizes the bundle size (since Interfaces are stripped off during bundling).

This mode is experimental and is being tested at the moment.

/modules

This flag helps in tree-shaking and code-splitting NSwag Clients.

By default NSwag generates http clients as Classes and puts all Classes in a single file. This prevents treeshaking, so even if you use a single method from class, whole class gets included in your bundle. Also since they are all in a single file, you can't code-split clients into chunks (all Clients will be loaded in a single chunk).

Now it's possible to fix it and generate NSwag Clients as functions (without Classes) splitted per file.

This comes with drawbacks, since some NSwag flags rely on Classes being used, so these options do not work with /modules flag. So if you use any of these, you won't be able to use the flag:

  • /baseClass (since there are no classes anymore)
  • /useGetBaseUrlMethod (since there is no base class)
  • /useTransformOptionsMethod (since there's no base class to define TransformOptions in), this might be implemented in future
  • /useTransformResultMethod (since there's no base class to define TransformOptions in), this might be implemented in future

You could use setBaseUrl and setAxiosFactory/setFetchFactory methods to configure the baseUrl and Axios/Fetch instances being used (which you previously configured via class constructors).

/no-hooks

Use this flag to disable generating react-query hooks.

You might want this flag if you want to use /modules, but you are not using react-query and don't need the generated hooks.

/fix-null-undefined-serialization

This flag executes few regex replaces over the generated code. This is an easy way to achieve the behavior we want without forking and maintaining NSwag & NJsonSchema templates ourselves.

Here are the regex rules and rationale behind them:

  • | undefined; is replaced by | null;

    Replaces DTO type definitions:

    export interface IUser {
       id?: number | undefined;   ->  id?: number | null;
    }
    

    Replace is made because this is what server (at least .NET :)) actually returns (at least by default)

  • : undefined is replaced by : null

    Changes init() function from:

    this.lastChangeDateTime = _data["lastChangeDateTime"] ? new Date(_data["lastChangeDateTime"].toString()) : <any>undefined;
    

    to

    this.lastChangeDateTime = _data["lastChangeDateTime"] ? new Date(_data["lastChangeDateTime"].toString()) : <any>null;
    

    Again, server actually returns null, we don't want to change that.

  • ? this.(...).toISOString() : null is replaced by && this.$1.toISOString()

    Performs the following change (in toJSON() method), from:

    data["shipDate"] = this.shipDate ? this.shipDate.toISOString() : <any>null;
    

    to

    data["shipDate"] = this.shipDate && this.shipDate.toISOString();
    

    This is to be able to send both undefined and null to the server (important for PATCH requests)

  • ? formatDate(...) : null is replaced by && formatDate(...)

    Performs the following change (in toJSON() method), from:

    data["shipDate"] = this.shipDate ? formatDate(this.shipDate) : <any>null;
    

    to

    data["shipDate"] = this.shipDate && formatDate(this.shipDate);
    

    This is to be able to send both undefined and null to the server (important for PATCH requests)

/use-recommended-configuration

This option basically passes the following parameters to NSwag /modules /fix-null-undefined-serialization /generateOptionalParameters:true /typeStyle:Class /markOptionalProperties:true /nullValue:undefined /generateConstructorInterface:true.

Here's a rationale behind each of them:

  • /generateOptionalParameters:true

    Otherwise, optional parameters are generated as mandatory. E.g.:

    • true: deletePet(petId: number, api_key?: string | null | undefined)
    • false: deletePet(petId: number, api_key: string | null | undefined) `
  • /typeStyle:Class

    Otherwise, if typeStyle is Inteface, there's no code to convert Date objects

  • /markOptionalProperties:true

    Otherwise PATCH dtos have all their properties defined as mandatory:

    export interface PatchUserDto {
        userName!: string | null;
        // should be: userName?: string | null;
    }
    
  • /nullValue:undefined

    If we use null as null value, unnecessary code gets added to .toJSON() and .init() functions:

    toJSON(data?: any) {
        data = typeof data === 'object' ? data : {};
    
        // nullValue:undefined
        data["enabled"] = this.enabled;
    
        // nullValue:null
        data["enabled"] = this.enabled !== undefined ? this.enabled : <any>null;
    }
    
    init(_data?: any) {
       if (_data) {
           // nullValue:undefined
           this.enabled = _data["enabled"];
    
           // nullValue:null
           this.enabled = _data["enabled"] !== undefined ? _data["enabled"] : <any>null;
       }
    }
    
  • /generateConstructorInterface:true

    This gives a typed-possibility to create classes from interfaces (otherwise you have to use init(_data?: any) method)

  • /fix-null-undefined-serialization

    We need this to be able to use both undefined and null as values in PATCH requests

/post-queries-start-with-get (or /non-get-query-condition)

By default we generate useQuery hooks for GET requests only. Though, sometimes backend uses POST queries to actually get the data (e.g. if request parameters are big and require HTTP BODY to send it). In this case if the name of your POST endpoints start with get, you could use /post-queries-start-with-get parameter, and we will generate useQuery hooks for them as well.

Alternatively, you could specify another flag /non-get-query-condition:CONDITION_HERE to determine which operations (beside GET) should be treated as GET (and thus have useQuery functions generated). Example of the condition: operation.ActualOperationName | downcase | slice: 0, 3 | replace: 'get', 'true' (it is actually used by default). Liquid template syntax is used here.

Upgrading instructions

From v14 to v15

There are some of breaking changes introduced in v15, because in v15 queries/mutations for each Controller is extracted into a separate file (and Classes are not used anymore).

  1. Getting/Setting default query properties is now done via functions (not via properties like it was before). So, instead of using AxiosQuery.Query.findPetsByStatusDefaultOptions property you'd need to use AxiosQuery.Query.getFindPetsByStatusDefaultOptions() and AxiosQuery.Query.setFindPetsByStatusDefaultOptions({/* options here */}).
  2. If you used Client property from the Query class to access POST/PUT methods (e.g. QueryFactory.Query.Client.addPet(...)), you'd be better off using QueryFactory.Client (together with /clients-as-modules flag), or QueryFactory.Query.Client() if you want to continue using Classes instead of Modules.
  3. If your API actions clash with JS reserved keywords your action would have underscore appended to the name (e.g. delete will be named delete_). Also in V15 it's possible to alter NSwag Clients to use plain functions instead of Classes. It makes treeshaking work for your Clients, thus significantly reducing the bundle size if you use only a few API methods. Use either /clients-as-modules flag directly, or /use-recommended-configuration which includes it.

How does it work

Under the cover it's just a couple of template files for NSwag and a small script to easily use them.

Contributions and support

Issues and Pull Requests are welcome.

For any kind of private consulting or support you could contact Artur Drobinskiy directly via email.