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).
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
- 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
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: [] }));
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 **/,
);
Sets base URL for all queries
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).
Sets the function to return the fetch
function 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.
You could define additional UseQueryOptions
for each query by calling set*QueryName*DefaultOptions
AxiosQuery.ClientQuery.setFindPetsByStatusDefaultOptions({
cacheTime: 10000
});
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.
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:
- All internal DTOs are JS classes, which are not recreated by
JSON.parse
(which is used by persisters by default). react-query-swagger
has Date objects in DTOs, which are not restored byJSON.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).
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') })
In addition to NSwag parameters we have 4 specific parameters:
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.
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).
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.
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
andnull
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
andnull
to the server (important for PATCH requests)
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)
`
- true:
-
/typeStyle:Class
Otherwise, if
typeStyle
isInteface
, there's no code to convertDate
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
andnull
as values in PATCH requests
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.
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).
- 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 useAxiosQuery.Query.getFindPetsByStatusDefaultOptions()
andAxiosQuery.Query.setFindPetsByStatusDefaultOptions({/* options here */})
. - 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 usingQueryFactory.Client
(together with/clients-as-modules
flag), orQueryFactory.Query.Client()
if you want to continue using Classes instead of Modules. - If your API actions clash with JS reserved keywords your action would have underscore appended to the name (e.g.
delete
will be nameddelete_
). 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.
Under the cover it's just a couple of template files for NSwag and a small script to easily use them.
Issues and Pull Requests are welcome.
For any kind of private consulting or support you could contact Artur Drobinskiy directly via email.