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

[![npm version](https://badge.fury.io/js/%407nohe%2Fopenapi-react-query-codegen.svg)](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 [@hey-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 @hey-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
