@7nohe/openapi-react-query-codegen
Version:
OpenAPI React Query Codegen
375 lines (299 loc) • 10.7 kB
Markdown
# OpenAPI React Query Codegen
> Node.js library that generates [React Query (also called TanStack Query)](https://tanstack.com/query) hooks based on an OpenAPI specification file.
[](https://badge.fury.io/js/%407nohe%2Fopenapi-react-query-codegen)
## Features
- Generates custom react hooks that use React Query's `useQuery`, `useSuspenseQuery`, `useMutation` and `useInfiniteQuery` hooks
- Generates custom functions that use React Query's `ensureQueryData` and `prefetchQuery` functions
- Generates query keys and functions for query caching
- Generates pure TypeScript clients generated by [-api/openapi-ts](https://github.com/hey-api/openapi-ts)
## Installation
```
$ npm install -D @7nohe/openapi-react-query-codegen
```
Register the command to the `scripts` property in your package.json file.
```json
{
"scripts": {
"codegen": "openapi-rq -i ./petstore.yaml -c axios"
}
}
```
You can also run the command without installing it in your project using the npx command.
```bash
$ npx --package @7nohe/openapi-react-query-codegen openapi-rq -i ./petstore.yaml -c axios
```
## Usage
```
$ openapi-rq --help
Usage: openapi-rq [options]
Generate React Query code based on OpenAPI
Options:
-V, --version output the version number
-i, --input <value> OpenAPI specification, can be a path, url or string content (required)
-o, --output <value> Output directory (default: "openapi")
-c, --client <value> HTTP client to generate (choices: "angular", "axios", "fetch", "node", "xhr", default: "fetch")
--request <value> Path to custom request file
--format <value> Process output folder with formatter? (choices: "biome", "prettier")
--lint <value> Process output folder with linter? (choices: "biome", "eslint")
--operationId Use operation ID to generate operation names?
--serviceResponse <value> Define shape of returned value from service calls (choices: "body", "response", default: "body")
--base <value> Manually set base in OpenAPI config instead of inferring from server value
--enums <value> Generate JavaScript objects from enum definitions? ['javascript', 'typescript', 'typescript+namespace']
--enums <value> Generate JavaScript objects from enum definitions? (choices: "javascript", "typescript")
--useDateType Use Date type instead of string for date types for models, this will not convert the data to a Date object
--debug Run in debug mode?
--noSchemas Disable generating JSON schemas
--schemaType <value> Type of JSON schema [Default: 'json'] (choices: "form", "json")
--pageParam <value> Name of the query parameter used for pagination (default: "page")
--nextPageParam <value> Name of the response parameter used for next page (default: "nextPage")
--initialPageParam <value> Initial value for the pagination parameter (default: "1")
-h, --help display help for command
```
### Example Usage
#### Command
```
$ openapi-rq -i ./petstore.yaml
```
#### Output directory structure
```
- openapi
- queries
- index.ts <- main file that exports common types, variables, and queries. Does not export suspense or prefetch hooks
- common.ts <- common types
- ensureQueryData.ts <- generated ensureQueryData functions
- queries.ts <- generated query hooks
- suspenses.ts <- generated suspense hooks
- prefetch.ts <- generated prefetch functions learn more about prefetching in in link below
- requests <- output code generated by -api/openapi-ts
```
- [Prefetching docs](https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr#prefetching-and-dehydrating-data)
#### In your app
##### Using the generated hooks
```tsx
// App.tsx
import { usePetServiceFindPetsByStatus } from "../openapi/queries";
function App() {
const { data } = usePetServiceFindPetsByStatus({ status: ["available"] });
return (
<div className="App">
<h1>Pet List</h1>
<ul>{data?.map((pet) => <li key={pet.id}>{pet.name}</li>)}</ul>
</div>
);
}
export default App;
```
##### Using the generated typescript client
```tsx
import { useQuery } from "@tanstack/react-query";
import { PetService } from "../openapi/requests/services";
import { usePetServiceFindPetsByStatusKey } from "../openapi/queries";
function App() {
// You can still use the auto-generated query key
const { data } = useQuery({
queryKey: [usePetServiceFindPetsByStatusKey],
queryFn: () => {
// Do something here
return PetService.findPetsByStatus(["available"]);
},
});
return <div className="App">{/* .... */}</div>;
}
export default App;
```
##### Using Suspense Hooks
```tsx
// App.tsx
import { useDefaultClientFindPetsSuspense } from "../openapi/queries/suspense";
function ChildComponent() {
const { data } = useDefaultClientFindPetsSuspense({ tags: [], limit: 10 });
return <ul>{data?.map((pet, index) => <li key={pet.id}>{pet.name}</li>)}</ul>;
}
function ParentComponent() {
return (
<>
<Suspense fallback={<>loading...</>}>
<ChildComponent />
</Suspense>
</>
);
}
function App() {
return (
<div className="App">
<h1>Pet List</h1>
<ParentComponent />
</div>
);
}
export default App;
```
##### Using Mutation hooks
```tsx
// App.tsx
import { usePetServiceAddPet } from "../openapi/queries";
function App() {
const { mutate } = usePetServiceAddPet();
const handleAddPet = () => {
mutate({ name: "Fluffy", status: "available" });
};
return (
<div className="App">
<h1>Add Pet</h1>
<button onClick={handleAddPet}>Add Pet</button>
</div>
);
}
export default App;
```
##### Invalidating queries after mutation
Invalidating queries after a mutation is important to ensure the cache is updated with the new data. This is done by calling the `queryClient.invalidateQueries` function with the query key used by the query hook.
Learn more about invalidating queries [here](https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation).
To ensure the query key is created the same way as the query hook, you can use the query key function exported by the generated query hooks.
```tsx
import {
usePetServiceFindPetsByStatus,
usePetServiceAddPet,
UsePetServiceFindPetsByStatusKeyFn,
} from "../openapi/queries";
// App.tsx
function App() {
const [status, setStatus] = React.useState(["available"]);
const { data } = usePetServiceFindPetsByStatus({ status });
const { mutate } = usePetServiceAddPet({
onSuccess: () => {
queryClient.invalidateQueries({
// Call the query key function to get the query key
// This is important to ensure the query key is created the same way as the query hook
// This insures the cache is invalidated correctly and is typed correctly
queryKey: [UsePetServiceFindPetsByStatusKeyFn({
status
})],
});
},
});
return (
<div className="App">
<h1>Pet List</h1>
<ul>{data?.map((pet) => <li key={pet.id}>{pet.name}</li>)}</ul>
<button
onClick={() => {
mutate({ name: "Fluffy", status: "available" });
}}
>
Add Pet
</button>
</div>
);
}
export default App;
```
##### Using Infinite Query hooks
This feature will generate a function in infiniteQueries.ts when the name specified by the `pageParam` option exists in the query parameters and the name specified by the `nextPageParam` option exists in the response.
The `initialPageParam` option can be specified to set the intial page to load, defaults to 1. The `nextPageParam` supports dot notation for nested values (i.e. `meta.next`).
Example Schema:
```yml
paths:
/paginated-pets:
get:
description: |
Returns paginated pets from the system that the user has access to
operationId: findPaginatedPets
parameters:
- name: page
in: query
description: page number
required: false
schema:
type: integer
format: int32
- name: tags
in: query
description: tags to filter by
required: false
style: form
schema:
type: array
items:
type: string
- name: limit
in: query
description: maximum number of results to return
required: false
schema:
type: integer
format: int32
responses:
'200':
description: pet response
content:
application/json:
schema:
type: object
properties:
pets:
type: array
items:
$ref: '#/components/schemas/Pet'
nextPage:
type: integer
format: int32
minimum: 1
```
Usage of Generated Hooks:
```ts
import { useDefaultServiceFindPaginatedPetsInfinite } from "@/openapi/queries/infiniteQueries";
const { data, fetchNextPage } = useDefaultServiceFindPaginatedPetsInfinite({
limit: 10,
tags: [],
});
```
##### Runtime Configuration
You can modify the default values used by the generated service calls by modifying the OpenAPI configuration singleton object.
It's default location is `openapi/requests/core/OpenAPI.ts` and it is also exported from `openapi/index.ts`
Import the constant into your runtime and modify it before setting up the react app.
```typescript
/** main.tsx */
import { OpenAPI as OpenAPIConfig } from './openapi/requests/core/OpenAPI';
...
OpenAPIConfig.BASE = 'www.domain.com/api';
OpenAPIConfig.HEADERS = {
'x-header-1': 'value-1',
'x-header-2': 'value-2',
};
...
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>
);
```
## Development
### Install dependencies
```bash
pnpm install
```
### Run tests
```bash
pnpm test
```
### Run linter
```bash
pnpm lint
```
### Run linter and fix
```bash
pnpm lint:fix
```
### Update snapshots
```bash
pnpm snapshot
```
### Build example and validate generated code
```bash
npm run build && pnpm --filter @7nohe/react-app generate:api && pnpm --filter @7nohe/react-app test:generated
```
## License
MIT