OASの内容を元に、Orvalで型やクライアントを生成する

少し前に Orval という型生成のツールを知り、少し触ってみていたのですがようやく触ってなんとなくわかったのでどんなことができたのかを書いておきます。

OAS は、サンプルで上がっていた petstore.yaml を使いました。

Orval

魚のアイコンが目印のジェネレーターで、TypeScriptのモデルやHTTP通信用のクライアント周りをreact-queryなど絡めつつ生成してくれたり、MSWのモックを生成できるツール。(今回MSWのモック生成はやってないのでまた今度)

TypeScriptで書かれているため、Open API Generatorみたく別途Javaなど他の言語をインストールする必要が無いというのは地味に嬉しいポイントでした。

セットアップ

ドキュメントに従いパッケージをインストール

$ npm i orval -D

これでorvalコマンドを使って生成していけるようになります。

使いやすいようにorval.config.tsファイルに書いておきます。

import { defineConfig } from "orval";

export default defineConfig({
  petstore: {
    input: "./openapi.yaml",
    output: {
      target: "src/libs/api/petstore.ts",
      schemas: "src/libs/api/schema",
      mode: "tags-split",
    },
  },
});

npm scriptsとして登録しておきます。

{
  :
  "scripts": {
    "generate:type": "orval --config ./orval.config.ts"
  },
  :
}

output.target だけの場合、axios を使ったリクエストの関数とひとまとまりになったschemaファイルが生成されます。
それだと型が探しにくかったり扱いづらさがあるので schemas とmodeを追加して場所を指定しつつある程度分割してファイル生成をするようにしています。

modeはいくつかありますが、tags か tags-list がいいかなと思っています。この辺りは順に少しみてみます。
各ディレクトリのリストは、 src/libs/api でのtreeコマンドによる生成結果です。

mode: tags の場合

├── pets.ts
└── schema
    ├── error.ts
    ├── index.ts
    ├── listPetsParams.ts
    ├── pet.ts
    └── pets.ts

少し分かりづらいですが、リクエストのファイルがタグごとに生成されつつschemaも別途生成されています。

mode: split の場合

├── petstore.ts
└── schema
    ├── error.ts
    ├── index.ts
    ├── listPetsParams.ts
    ├── pet.ts
    └── pets.ts

リクエストのファイルがorval.config.tsのトップレベルで定義している名前でひとまとまりになりつつ、schemaが分割されました。

mode: tags-split の場合

├── pets
│   └── pets.ts
└── schema
    ├── error.ts
    ├── index.ts
    ├── listPetsParams.ts
    ├── pet.ts
    └── pets.ts

pets/ にリクエストの関数の入ったファイルがが入っている状態です。
この階層がファイルやディレクトリでバラバラにならないのはよさそうです。

生成後の後処理も追加できる

hooks.afterAllFilesWriteでファイル生成にフックしてなにかを行うことができます。

{
  :
  input: { ... },
  output: { ... },
  hooks: {
    afterAllFilesWrite: "ファイル生成後に行いたい処理"
  }

ここでprettierを使ってformatかけるようにしています。対象ファイルもoutput内に絞ればそこまで時間はかからなそうです。

React Queryを指定してClientを生成する

キャッシュ機構などを使いたいのでReact-Queryを使うことが多いです。
Orvalを使うとReact-QueryのuseQueryとリクエスト関数を紐付けたカスタムフックやその際に使用するkeyの生成もやってくれます。

{
  :
  output: {
    :
    client: "react-query"
  },
}

生成内容がまずまず増えるので全て貼るのは控えますが、こんな感じのカスタムフックが生成されたりします。

export const useListPets = <TData = Awaited<ReturnType<typeof listPets>>, TError = AxiosError<Error>>(
 params?: ListPetsParams, options?: { query?:UseQueryOptions<Awaited<ReturnType<typeof listPets>>, TError, TData>, axios?: AxiosRequestConfig}

  ):  UseQueryResult<TData, TError> & { queryKey: QueryKey } => {

  const queryOptions = getListPetsQueryOptions(params,options)

  const query = useQuery(queryOptions) as  UseQueryResult<TData, TError> & { queryKey: QueryKey };

  query.queryKey = queryOptions.queryKey ;

  return query;
}

clientに指定するだけでここまでやってくれるのでありがたいですね。

ちょっと大変な点

根っこでaxiosを使ってくれているのですが、 interceptors でリクエストやレスポンスの加工や前処理なんかを行う事は度々あるかと思いますが、 interceptors を指定する際のaxiosのインスタンスを指定する方法が分からず困りました。

結果的には axios のカスタムインスタンスを設定する項を参考に自作のインスタンスを設定して解決しました。

const AXIOS_INSTANCE = axios.create(axiosConfig());

export const customInstance = <T>(
  config: AxiosRequestConfig,
  options?: AxiosRequestConfig
): Promise<T> => {
  const promise = AXIOS_INSTANCE({
    ...config,
    ...options,
  }).then(({ data }) => data);

  AXIOS_INSTANCE.interceptors.request.use(
  :

カスタムインスタンスの型が決まっているのでそこに沿うように設定したのですが、このコードのようになり成功前提になってしまうのでそこだけなんとかならないかなーと思っています。
React-Query側で取れるよねって話だとは思いますが…

使ってみて

少ない設定である程度作業がスキップできる状態になるのでありがたいツールだなと思います。
ただ、決まりごとも多いのでその辺りとうまく向き合えるようにするのだけ頑張らないとですね。

そのうちMSWのハンドラ生成もやってみようと思います。