Orvalを利用して、OpenAPI v3からtanstack-library, client-library, zod, mswを自動生成する

# Open API3から、react-query, zod, mswを生成する

[Orval](https://orval.dev/)を利用してコードを生成していきます。

Generate, valid, cache and mock in your frontend applications all with your OpenAPI specification.

OpenAPI v3を利用して、client libraryだけじゃなく、msw、zodやtanstack-query,swrなどを生成してくれるgeneratorです。
[Typespec](https://github.com/orval-labs/orval/issues/434)については、discussion中となっております。

## 0. before do

想定しているディレクトリ構成とファイルの中身。

```terminal
tree . -I node_modules
.
├── README.md
├── compose.yaml
├── dockerfile
├── openapi3
│   ├── generated
│   │   └── @typespec
│   │       └── openapi3
│   │           └── openapi.yaml
├── package-lock.json
├── package.json
```

<details><summary>compose.yaml</summary>

```compose.yaml
services:
  http_schema_container:
    container_name: http_schema_container
    build:
      context: .
      dockerfile: dockerfile
    tty: true
    restart: always
    volumes:
      - .:/usr/src/app
```

</details>

<details><summary>dockerfile</summary>

```dockerfile
FROM node:23-alpine3.19

WORKDIR /usr/src/app
```

</details>

<details><summary>openapi3/generated/@typespec/openapi3/openapi.yaml</summary>

```openapi3/generated/@typespec/openapi3/openapi.yaml
openapi: 3.0.0
info:
  title: (title)
  version: 0.0.0
tags: []
paths:
  /pets:
    get:
      operationId: Pets_list
      parameters:
        - name: filter
          in: query
          required: true
          schema:
            type: string
          explode: false
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Pet'
    post:
      operationId: Pets_create
      parameters: []
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Pet'
  /pets/{id}:
    get:
      operationId: Pets_read
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Pet'
  /stores:
    get:
      operationId: Stores_list
      parameters:
        - name: filter
          in: query
          required: true
          schema:
            type: string
          explode: false
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/Store'
  /stores/{id}:
    get:
      operationId: Stores_read
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: The request has succeeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Store'
components:
  schemas:
    Address:
      type: object
      required:
        - street
        - city
      properties:
        street:
          type: string
        city:
          type: string
    Pet:
      type: object
      required:
        - name
        - age
      properties:
        name:
          type: string
        age:
          type: integer
          format: int32
    Store:
      type: object
      required:
        - name
        - address
      properties:
        name:
          type: string
        address:
          $ref: '#/components/schemas/Address'

```


</details>


## 1. Orvalの依存関係とConfigファイルを生成する

```
% npm i orval -D
% touch orval.config.ts
```

```orval.config.ts
import { defineConfig } from 'orval';
 
 export default defineConfig({
   petstore: {
     input: './openapi3/generated/@typespec/openapi3/openapi.yaml',
     output: './src/gen/petstore.ts',
     hooks: {
       afterAllFilesWrite: 'prettier --write',
     },
   },
 });
```

```package.json
"scripts": {
    "gen:orval": "orval --config ./orval.config.ts",
}
```

```terminal
% npm run gen:orval
```

```
% tree . -I node_modules
.
├── README.md
├── compose.yaml
├── dockerfile
├── models
│   └── store.tsp
├── openapi3
│   ├── generated
│   │   └── @typespec
│   │       └── openapi3
│   │           └── openapi.yaml
├── orval.config.ts
├── package-lock.json
├── package.json
└── src
    └── gen
        └── petstore.ts
```

```src/gen/petstore.ts
/**
 * Generated by orval v7.1.1 🍺
 * Do not edit manually.
 * (title)
 * OpenAPI spec version: 0.0.0
 */
import axios from "axios";
import type { AxiosRequestConfig, AxiosResponse } from "axios";
export type StoresListParams = {
  filter: string;
};

export type PetsListParams = {
  filter: string;
};

export interface Store {
  address: Address;
  name: string;
}

export interface Pet {
  age: number;
  name: string;
}

export interface Address {
  city: string;
  street: string;
}

export const petsList = <TData = AxiosResponse<Pet[]>>(
  params: PetsListParams,
  options?: AxiosRequestConfig,
): Promise<TData> => {
  return axios.get(`/pets`, {
    ...options,
    params: { ...params, ...options?.params },
  });
};

export const petsCreate = <TData = AxiosResponse<Pet>>(
  pet: Pet,
  options?: AxiosRequestConfig,
): Promise<TData> => {
  return axios.post(`/pets`, pet, options);
};

export const petsRead = <TData = AxiosResponse<Pet>>(
  id: string,
  options?: AxiosRequestConfig,
): Promise<TData> => {
  return axios.get(`/pets/${id}`, options);
};

export const storesList = <TData = AxiosResponse<Store[]>>(
  params: StoresListParams,
  options?: AxiosRequestConfig,
): Promise<TData> => {
  return axios.get(`/stores`, {
    ...options,
    params: { ...params, ...options?.params },
  });
};

export const storesRead = <TData = AxiosResponse<Store>>(
  id: string,
  options?: AxiosRequestConfig,
): Promise<TData> => {
  return axios.get(`/stores/${id}`, options);
};

export type PetsListResult = AxiosResponse<Pet[]>;
export type PetsCreateResult = AxiosResponse<Pet>;
export type PetsReadResult = AxiosResponse<Pet>;
export type StoresListResult = AxiosResponse<Store[]>;
export type StoresReadResult = AxiosResponse<Store>;
```

## 2. 利用するLibraryを設定していく

[React Query](https://orval.dev/guides/react-query)、[msw](https://orval.dev/guides/react-query)、[zod](https://orval.dev/guides/zod)の設定を追加してコードを生成する。

### 2-1. Zodの設定を追加する

Zodの設定を追加して、codeを生成していく。

```orval.config.ts
import { defineConfig } from 'orval';

const OpenAPIFilePath = "./openapi3/generated/@typespec/openapi3/openapi.yaml"
const OutPutDir = "./src/gen"
 
 export default defineConfig({
    pestore: {
        input: {
            target: OpenAPIFilePath
        },
        output: `${OutPutDir}/petstore.ts`
    },
    petstoreZod: {
        input: {
            target: OpenAPIFilePath
        },
        output: {
            client: "zod",
            mode: 'tags-split',
            target: `${OutPutDir}/zod`,
            fileExtension: '.zod.ts',
        }
    }
});
```

```terminal
% npm run gen:orval
% tree . -I node_modules
.
├── README.md
├── compose.yaml
├── dockerfile
├── models
│   └── store.tsp
├── openapi3
│   ├── generated
│   │   └── @typespec
│   │       └── openapi3
│   │           └── openapi.yaml
├── orval.config.ts
├── package-lock.json
├── package.json
└── src
    └── gen
        ├── petstore.ts
        └── zod
            └── default
                └── default.zod.ts
```

### 2-2. React QueryとMSWを生成する

React Queryとmswに設定する

```orval.config.ts
import { defineConfig } from 'orval';

const OpenAPIFilePath = "./openapi3/generated/@typespec/openapi3/openapi.yaml"
const OutPutDir = "./src/gen"
 
 export default defineConfig({
    pestore: {
        input: {
            target: OpenAPIFilePath
        },
        output: {
            mode: 'tags-split',
            target: `${OutPutDir}/petstore.ts`,
            schemas: `${OutPutDir}/model`,
            client: 'react-query',
            mock: true,
        }
    },
    petstoreZod: {
        input: {
            target: OpenAPIFilePath
        },
        output: {
            client: "zod",
            mode: 'tags-split',
            target: `${OutPutDir}/zod`,
            fileExtension: '.zod.ts',
        }
    }
});

```

```terminal
% npm run gen:orval
% tree . -I node_modules
.
├── README.md
├── compose.yaml
├── dockerfile
├── models
│   └── store.tsp
├── openapi3
│   ├── generated
│   │   └── @typespec
│   │       └── openapi3
│   │           └── openapi.yaml
├── orval.config.ts
├── package-lock.json
├── package.json
└── src
    └── gen
        ├── default
        │   ├── default.msw.ts
        │   └── default.ts
        ├── model
        │   ├── address.ts
        │   ├── index.ts
        │   ├── pet.ts
        │   ├── petsListParams.ts
        │   ├── store.ts
        │   └── storesListParams.ts
        ├── petstore.ts
        └── zod
            └── default
                └── default.zod.ts
```