【Orval】TypeSpec経由でAPIクライアントとSWRフックを生成

2024年7月9日
【Orval】TypeSpec経由でAPIクライアントとSWRフックを生成

前回、TypeSpecでAPI定義した後、OpenAPIのYAMLファイルを自動生成できるようにした。

OpenAPI定義を生成できたので、API用のクライアントコード、 さらにはReactアプリからAPIのデータ取得用のカスタムフックも自動生成したい。

Orvalを使うとSWRのAPIアクセスのためのhookも自動生成できるようなので試してみる。

SWRとは

SWRはデータ取得のためのReact hooksライブラリで、 APIアクセスのためのキャッシュやエラー処理などの機能を提供する。

SWRを使うことで、フロントからAPIリクエストする際に必要になる機能を簡単に実装できる。

以下は、ドキュメントに記載されている例。

import useSWR from 'swr'

function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher)

  if (error) return <div>failed to load</div>
  if (isLoading) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

useSWRフックを使って、/api/userエンドポイントからデータを取得している。

Orvalとは

OrvalはOpenAPI(Swagger)仕様から、TypeScriptのクライアントコードを生成するツールで、以下の特徴がある。

  1. TypeScriptモデルの生成
  2. HTTP呼び出しの生成
  3. MSW(Mock Service Worker)を使用したモックの生成

SWRのHooksも生成できる他、React Query、Angularなどのクライアントに対応している。

インストール

まずはOrvalをインストールする。

npm i orval -D

準備

生成するにあたって、TypeSpecのtspファイルとorvalの設定ファイルを用意する。

main.tsp

TypeSpecでシンプルなAPI定義を作成する。

import "@typespec/http";

using TypeSpec.Http;

model User {
	id: string;
	name: string;
	email: string;
}

@route("/users")
interface Users {
	@get
	getUsers(): User[];

	@get
	@route("/{id}")
	getUser(@path id: string): User;
}

前回同様の設定で、openapi.ymlを出力する。

$ tsp compile main.tsp --emit @typespec/openapi3

orval.config.ts

今回はSWRを作成したいので、次のドキュメントが参考になる。

https://orval.dev/guides/swr

orval.config.tsを作成して出力設定を記載する。

module.exports = {
  userstore: {
    input: {
      target: "./openapi.yml",
    },
    output: {
      mode: "split",
      target: "./api/endpoints",
      schemas: "./api/models",
      client: "swr",
    },
  },
};

今回はapiディレクトリ以下に、モデル定義はmodels、SWRファイルはendpointsディレクトリに配置されるようにした。

生成したファイルにprettierを適用

以下の設定で、生成したファイルにprettierを適用できる。

https://orval.dev/reference/configuration/hooks#afterallfileswrite

module.exports = {
  userstore: {
    input: {
      target: "./openapi.yml",
    },
    output: {
      mode: "split",
      target: "./api/endpoints",
      schemas: "./api/models",
      client: "swr",
    },
+   hooks: {
+     afterAllFilesWrite: "prettier --write",
+   },
  },
};

実行

以下のコマンドでOrvalを実行する。

$ npx orval
🍻 Start orval v6.31.0 - A swagger client generator for typescript
(node:4636) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
🎉 user-api - Your OpenAPI spec has been converted into ready to use orval!

warningが発生したが、api以下に生成ファイルを出力することができた。

models/user.ts

API で使用されるデータモデルの型定義を含んでいる。 TypeSpecのモデル定義が反映されていることがわかる。

/**
 * Generated by orval v6.31.0 🍺
 * Do not edit manually.
 * User API
 * OpenAPI spec version: 0.0.0
 */

export interface User {
  email: string;
  id: string;
  name: string;
}

endpoints/userAPI.ts

API エンドポイントに対応するSWRフックが自動生成される。

/**
 * Generated by orval v6.31.0 🍺
 * Do not edit manually.
 * User API
 * OpenAPI spec version: 0.0.0
 */
import axios from "axios";
import type { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
import useSwr from "swr";
import type { Key, SWRConfiguration } from "swr";
import type { User } from "../models";

type AwaitedInput<T> = PromiseLike<T> | T;

type Awaited<O> = O extends AwaitedInput<infer T> ? T : never;

export const usersGetUsers = (
  options?: AxiosRequestConfig
): Promise<AxiosResponse<User[]>> => {
  return axios.get(`/users`, options);
};

export const getUsersGetUsersKey = () => [`/users`] as const;

export type UsersGetUsersQueryResult = NonNullable<
  Awaited<ReturnType<typeof usersGetUsers>>
>;
export type UsersGetUsersQueryError = AxiosError<unknown>;

export const useUsersGetUsers = <TError = AxiosError<unknown>>(options?: {
  swr?: SWRConfiguration<Awaited<ReturnType<typeof usersGetUsers>>, TError> & {
    swrKey?: Key;
    enabled?: boolean;
  };
  axios?: AxiosRequestConfig;
}) => {
  const { swr: swrOptions, axios: axiosOptions } = options ?? {};

  const isEnabled = swrOptions?.enabled !== false;
  const swrKey =
    swrOptions?.swrKey ?? (() => (isEnabled ? getUsersGetUsersKey() : null));
  const swrFn = () => usersGetUsers(axiosOptions);

  const query = useSwr<Awaited<ReturnType<typeof swrFn>>, TError>(
    swrKey,
    swrFn,
    swrOptions
  );

  return {
    swrKey,
    ...query,
  };
};

export const usersGetUser = (
  id: string,
  options?: AxiosRequestConfig
): Promise<AxiosResponse<User>> => {
  return axios.get(`/users/${id}`, options);
};

export const getUsersGetUserKey = (id: string) => [`/users/${id}`] as const;

export type UsersGetUserQueryResult = NonNullable<
  Awaited<ReturnType<typeof usersGetUser>>
>;
export type UsersGetUserQueryError = AxiosError<unknown>;

export const useUsersGetUser = <TError = AxiosError<unknown>>(
  id: string,
  options?: {
    swr?: SWRConfiguration<Awaited<ReturnType<typeof usersGetUser>>, TError> & {
      swrKey?: Key;
      enabled?: boolean;
    };
    axios?: AxiosRequestConfig;
  }
) => {
  const { swr: swrOptions, axios: axiosOptions } = options ?? {};

  const isEnabled = swrOptions?.enabled !== false && !!id;
  const swrKey =
    swrOptions?.swrKey ?? (() => (isEnabled ? getUsersGetUserKey(id) : null));
  const swrFn = () => usersGetUser(id, axiosOptions);

  const query = useSwr<Awaited<ReturnType<typeof swrFn>>, TError>(
    swrKey,
    swrFn,
    swrOptions
  );

  return {
    swrKey,
    ...query,
  };
};
  1. axios を使用して API リクエストを行う関数が定義されている。
  • usersGetUsers(): すべてのユーザーを取得
  • usersGetUser(id: string): 特定のユーザーを取得
  1. SWR フックが各エンドポイントに対して生成されている。
  • useUsersGetUsers(): すべてのユーザーを取得するフック
  • useUsersGetUser(id: string): 特定のユーザーを取得するフック

たとえば、以下のようにして、ユーザー情報を取得するできる。

const { data, error, isLoading } = useUsersGetUsers();

swr オプションを使用して、SWR フックの設定を変更できる。 たとえば、キャッシュを無効にする場合は、enabled: falseを設定する。

const { data, error, isLoading } = useUsersGetUsers({
  swr: {
    enabled: false,
  },
});

APIのBaseURLを変更

デフォルトの設定では、生成したAPIクライアントはフロントと同じドメインのエンドポイントにリクエストするようになっている。 実際にはフロントから別のドメインのAPIにリクエストしたい場合がある。

その場合、Orvalで自動生成したSWRは内部的にaxiosを使ってHTTPリクエストをしているので、 以下のようにaxiosのデフォルトのbaseURLを変更することで、リクエスト先のドメインを変更できる。

axios.defaults.baseURL = '<BACKEND URL>';

他の方法など、詳しくは以下のページを参照。

まとめ

以下のプロセスでフロントのAPIクライアントを自動生成できることがわかった。

  1. TypeSpecのtspファイルからOpenAPI定義ファイルを生成
  2. Orvalを使用して、生成したOpenAPI定義からフロントエンド用のSWRフックを自動作成

また、MSWのモックも自動生成できるので、バックエンドが未完成の場合でも、 ひとまずTypeSpecのAPI定義を作れば、自動生成したSWRのフックでフロントエンドの開発を進められる。

次の記事はこちら。