🐬

OpenAPIからコードを自動生成!『Orval』のメリットと選定理由

2023/03/13に公開

はじめに

本記事ではOpenAPIからclientコードを自動生成するライブラリ『Orval』を紹介します。そしてOrvalの強みを明らかにし、どのような場合に選定するべきかを提案できたらと考えます。

https://orval.dev/

いきなりですが簡潔に結論を述べます。

これはOrvalの作者自身がswagger codegenといった他の自動生成ライブラリに物足りなさを感じ、特定の技術の上で最適なコード生成をしたいというモチベーションが根底にあります。

Orvalとは?

改めてOrvalはOpenAPIからclientコードを自動生成するライブラリです。(公式によると)その特徴は、大きく3つ挙げられます。
スクリーンショット 2023-03-05 21.36.57.png

Productivity

Save time to drink a 🍺, get your api ready out of the box in a few seconds. Prevent human error, be sure on the return result by imposing standard formatting.

ビールを飲む時間も与えないほど早く、数秒にしてAPI処理のコードを自動生成します!また、標準的な形式を適用することでヒューマンエラーを防ぐことができます。

Error boundary

Get your contract ! With the combination of orval and openapi, you have a strong standard for your team which avoid any problem of missunderstanding and give you the possibility to focus on your ui.

契約書を手に入れよう!OrvalとOpenAPIの組み合わせによって、チームで誤解を生むリスクを減らしよりUIに集中することができるようになります。

API mocking

Don't wait for your backend to be ready to test your application. Generate your mocks with Orval, knowing that you are ready to be connected to your API.

アプリケーションのテストをする際にbackendの実装を待つ必要はありません。Orvalでmockを生成し、APIと接続を確認しましょう。

🤙 特徴1: TanStack Queryとの相性がいい!

私はよく業務や個人開発でTanStack Queryを状態管理ライブラリとして採用しますが、実装が面倒なカスタムフックを自動生成してくれるという観点でとても相性がいいです。

Orvalの設定でclientオプションにreact-queryを指定するだけで、全てのエンドポイントに対応するカスタムフックを作成してくれます。

orval.config.js
 module.exports = {
   petstore: {
     output: {
       // これだけ
       client: 'react-query',
     },
   },
 };
自動生成されるコード
 export const showPetById = (
   petId: string,
   options?: AxiosRequestConfig,
 ): Promise<AxiosResponse<Pet>> => {
   return axios.get(`/pets/${petId}`, options);
 };
 
 export const getShowPetByIdQueryKey = (petId: string) => [`/pets/${petId}`];
 
 export const useShowPetById = <
   TData = AsyncReturnType<typeof showPetById>,
   TError = Error,
 >(
   petId: string,
   options?: {
     query?: UseQueryOptions<AsyncReturnType<typeof showPetById>, TError, TData>;
     axios?: AxiosRequestConfig;
   },
 ) => {
   const { query: queryOptions, axios: axiosOptions } = options ?? {};
 
   const queryKey = queryOptions?.queryKey ?? getShowPetByIdQueryKey(petId);
   const queryFn = () => showPetById(petId, axiosOptions);
 
   const query = useQuery<AsyncReturnType<typeof queryFn>, TError, TData>(
     queryKey,
     queryFn,
     { enabled: !!petId, ...queryOptions },
   );
 
   return {
     queryKey,
     ...query,
   };
 };

私は以前TanStack Query設計のベストプラクティスとしてこのような記事を書きました。

https://qiita.com/taisei-13046/items/37d685ff07b561881935

ご一読いただけると幸いですが簡潔にまとめると、TanStack Queryの設計に重要な観点は以下の4つだと考えています。

  • 各エンドポイントに対応するuseQueryの実装を共通化する
  • Query Keyを一意に保つ
  • Query FnをuseQueryから分離させる
  • useQueryのhooksに対してoptionsを指定できるようにする

それぞれOrvalの自動生成コードがこの要件を満たしているか確認しましょう。
注)ここではuseQueryと限定して記載していますが、useMutationやuseInfiniteQueryなどについても同様のことが言えます。

各エンドポイントに対応するuseQueryの実装を共通化する

REST設計を採用している場合、各エンドポイントに対応するuseQueryの処理は共通化できます。仮にOrvalを採用しなかったとしても、このような実装は共通化するべきでしょう。その点OrvalはTanStack Queryのカスタムフックをエンドポイントごとに作成してくれます。
また、設定でmodeオプションにtags-splitを指定すると、生成されるディレクトリ構成をエンドポイントに合わせてくれるのでとても見通しが良くなります。

├ users
├  ├ users.msw.ts
├  └ users.ts
└ posts
   ├ posts.msw.ts
   └ posts.ts

Query Keyを一意に保つ

Query Keyを一意に保つことはTanStack Queryのキャッシュ機構を活用する観点でとても重要です。しかしその反面、Query Keyを自分たちで管理することは面倒に感じることがあります。そこでKeyの管理はOrvalに任せてしまいましょう!Orvalの自動生成されるコードはQuery Keyを返す関数をexportしてくれています。これによってinvalidateなどの処理でKeyが必要になったとしても対応することができます。

 export const getShowPetByIdQueryKey = (petId: string) => [`/pets/${petId}`];

https://tanstack.com/query/v4/docs/react/guides/query-invalidation

Query FnをuseQueryから分離させる

TanStack Queryは正確にはデータフェッチライブラリではありません。なぜなら、useQueryのQuery Fnにはfetchやaxios、kyといったライブラリを使用する必要があり、実際にserverからデータを取得する役割はこれらが担っています。つまり、useQueryが完全にaxiosなどのライブラリに依存することは避けたいです。その点OrvalはQuery Fnを分けて実装していますし、 HTTP Clientをカスタマイズすることも可能と言う点で優れています!

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

useQueryのhooksに対してoptionsを指定できるようにする

useQueryの実装をカスタムフック化する際に気をつけるべきことは、useQueryの強力なオプションを失わないことです。オプションの効力を失わないためには適切な型定義が必要になりますが、この点に関してもOrvalはクリアしています!

 export const useShowPetById = <
   TData = AsyncReturnType<typeof showPetById>,
   TError = Error,
 >(
   petId: string,
   options?: {
     query?: UseQueryOptions<AsyncReturnType<typeof showPetById>, TError, TData>;
     axios?: AxiosRequestConfig;
   },
 ) => {
   // ...省略
 
   return {
     queryKey,
     ...query,
   };
 };

以上からわかる通り、Orvalで自動生成されるコードはTanStack Queryの強みを最大限に生かした設計がされておりその効力を失う懸念はありません!また、このように誰が実装しても似たような結果になる処理に関しては自動生成されるべきだと考えています。それにより、意図しないヒューマンエラーや保守コストを削減できます。

🤙 特徴2: Mockの自動生成が強い!

設定でmockオプションをtrueにすることで、便利なmock関数を自動生成してくれます!昨今のフロントエンドテストでは、結合テストにmswを使う例がよくみられますがOrvalもAPIのmockにmswを採用しています。またmockデータの生成にfakerを採用していることからも、よりリアルデータに近い形でのテストが可能になります。

mock関数を自前で用意することはとても面倒でメンテナンスコストが高くなります。そういった意味でもOrvalにmock関数の生成を任せることでヒューマンエラーを防ぎ、よりUIの実装に集中することができます。

https://mswjs.io/

https://fakerjs.dev/

orval.config.js
 module.exports = {
   petstore: {
     output: {
       mock: true,
     },
   },
 };
自動生成されるコード
/**
 * Generated by orval v6.12.1 🍺
 * Do not edit manually.
 * Swagger Petstore
 * OpenAPI spec version: 1.0.0
 */
import { rest } from 'msw';
import { faker } from '@faker-js/faker';

export const getListPetsMock = () =>
  Array.from(
    { length: faker.datatype.number({ min: 1, max: 10 }) },
    (_, i) => i + 1,
  ).map(() =>
    faker.helpers.arrayElement([
      {
        cuteness: faker.datatype.number({ min: undefined, max: undefined }),
        breed: faker.helpers.arrayElement(['Labradoodle']),
        barksPerMinute: faker.helpers.arrayElement([
          faker.datatype.number({ min: undefined, max: undefined }),
          undefined,
        ]),
        type: faker.helpers.arrayElement(['dog']),
      },
      {
        length: faker.datatype.number({ min: undefined, max: undefined }),
        breed: faker.helpers.arrayElement(['Dachshund']),
        barksPerMinute: faker.helpers.arrayElement([
          faker.datatype.number({ min: undefined, max: undefined }),
          undefined,
        ]),
        type: faker.helpers.arrayElement(['dog']),
        '@id': faker.helpers.arrayElement([faker.random.word(), undefined]),
        id: (() => faker.datatype.number({ min: 1, max: 99999 }))(),
        name: (() => faker.name.lastName())(),
        tag: (() => faker.name.lastName())(),
        email: faker.helpers.arrayElement([faker.internet.email(), undefined]),
        callingCode: faker.helpers.arrayElement([
          faker.helpers.arrayElement(['+33', '+420', '+33']),
          undefined,
        ]),
        country: faker.helpers.arrayElement([
          faker.helpers.arrayElement(["People's Republic of China", 'Uruguay']),
          undefined,
        ]),
      },
      {
        petsRequested: faker.helpers.arrayElement([
          faker.datatype.number({ min: undefined, max: undefined }),
          undefined,
        ]),
        type: faker.helpers.arrayElement(['cat']),
        '@id': faker.helpers.arrayElement([faker.random.word(), undefined]),
        id: (() => faker.datatype.number({ min: 1, max: 99999 }))(),
        name: (() => faker.name.lastName())(),
        tag: (() => faker.name.lastName())(),
        email: faker.helpers.arrayElement([faker.internet.email(), undefined]),
        callingCode: faker.helpers.arrayElement([
          faker.helpers.arrayElement(['+33', '+420', '+33']),
          undefined,
        ]),
        country: faker.helpers.arrayElement([
          faker.helpers.arrayElement(["People's Republic of China", 'Uruguay']),
          undefined,
        ]),
      },
    ]),
  );

export const getSwaggerPetstoreMSW = () => [
  rest.get('*/v:version/pets', (_req, res, ctx) => {
    return res(
      ctx.delay(1000),
      ctx.status(200, 'Mocked status'),
      ctx.json(getListPetsMock()),
    );
  }),
];

🤙 特徴3: カスタマイズ性が高い!

Orvalの設定ファイルはカスタマイズ性が高いことも大きなメリットと考えられます!

https://orval.dev/reference/configuration/overview

modeオプション

ファイルのアウトプット方法には4種類のmodeが存在します。
single, split, tags, tags-split

single

Defaultで設定されています。
一つのファイルに全てまとめて出力される。

 my-app
 └── src
     └── api
         └── endpoints
             └── petstore.ts

split

definition, implementation, schemas, mockがそれぞれ別のファイルに出力される。

 my-app
 └── src
     ├── petstore.definition.ts
     ├── petstore.schemas.ts
     ├── petstore.msw.ts
     └── petstore.ts

tags

OpenAPIのtagごとにまとめて出力される。

 my-app
 └── src
     ├── pets.ts
     └── petstore.schemas.ts

tags-split

tag modeとsplit modeの組み合わせ。
tagごとにディレクトリを分け、definition, implementation, schemas, mockがそれぞれ別ファイルとして出力される。

 my-app
 └── src
     ├── petstore.schemas.ts
     └── pets
         ├── petstore.ts
         ├── petstore.definition.ts
         ├── petstore.msw.ts
         └── petstore.ts

overrideオプション

overrideオプションを使用すると、Defaultで設定されているOrvalの挙動を上書きすることができます。オプションの種類が数多く存在するので、詳しくは公式を参照ください。

https://orval.dev/reference/configuration/output#override

例えばPOSTメソッドをセキュリティの観点などからデータ取得のために使用した場合、OrvalはDefaultでuseMutationを採用してしまいます。しかし、データの作成ではなくデータを取得する目的でPOSTを使用しているので、実際はuseQueryを使用しQueryKeyによってキャッシュ管理できるように出力して欲しいです。その場合は以下のように、設定を上書きしましょう。

orval.config.js
 module.exports = {
   petstore: {
     output: {
       override: {
         operations: {
           // 特定のoperationIdを指定
           getPetList: {
             query: {
               useQuery: true
             },
           },
         },
       },
     },
   },
 };

このようにして特定のエンドポイントのみDefaultの挙動を上書きするなどの柔軟な対応をすることが可能です。

参考

https://github.com/anymaniax/orval/issues/515

まとめ

以上Orvalの特徴とその強みを紹介しました。

Orvalを採用するメリットはTanStack Queryなどのコード自動生成によってヒューマンエラーを防ぎ、よりUIの実装に集中できるようになることです。冒頭でも述べた通り、OpeanAPIからのコード生成ライブラリはOrval以外にも存在しますが、特定の技術スタックに特化している点でOrvalは優れています。逆に言えば、状態管理でReduxなどを採用している場合はその恩恵を受けづらいでしょう。「痒い所に手が届く、そして私たちの開発効率を大幅に向上させてくれる」。Orvalはそんなライブラリだと考えます。

皆さんも、Orvalを使って快適な開発をしましょう!

株式会社HRBrain

Discussion