とろろこんぶろぐ

かけだしR&Dフロントエンジニアの小言

Zod のスキーマが使えるAPIクライアントZodios を紹介したい

概要

TypeScriptでフロントエンド開発をしているとバックエンドのAPIを呼び出す際に、APIのパラメータ、レスポンスの型付けをしたくなります。

僕は最近この型付けにZodを使い、APIクライアントにはZodiosというライブラリを使っています。

github.com

この記事では、Zodios でZodのスキーマ定義から型安全なAPIクライアントを作る方法を紹介します。

他のやり方

Types定義

純粋にパラメータとレスポンスの type を Type Alias で定義し、fetch やaxios などの素のAPIクライアントに型付けするやり方です。 APIのパスと、パラメータ、レスポンスの紐付けを人間が管理することになるため、ミスを防ぐことができません。

aspida

この課題のためにaspidaがよく使われている印象があります。

github.com

Open APIからaspidaのAPIクライアントのコードが書かれているファイルを自動生成できるので、バックエンド側でOpen APIがすでに用意されているプロジェクトではとても有用だと思います。

自動生成されたファイルから適切なクライアントをimport することで、パス、クエリ、レスポンスといった型補完を効かせながらクライアントを記述できます。

またOpen APIがなくても、あらかじめ決められた型の形式でAPIの型定義を書くことで、その型定義を元にAPIクライアントが記載されたファイルを自動生成してくれます。

課題に対して効果的な解決策になります。

ただ、(好みの問題ではありますが)書き味や使い方に少し癖があります。 APIクライアントのメソッド名は自動的に作られたものを利用することになります。 ファイルを自動生成させるため、npm scriptにaspidaのビルドスクリプトを追加して、watch しておく必要があります。

gRPC, tRPC

バックエンド側の開発に制約がなければgRPCや最近流行りのtRPCを利用することで、バックエンド側で記載されるAPI定義をそのままフロントエンドを型として利用する方法もあります。

ちなみにZodiosでもzodios-expressを利用すれば、tRPC的に開発することもできます。

GraphQL

同様にバックエンド側で開発可能であればGraphQLを使う方法もあります。Schemaを定義することで、バックエンドとフロントエンドで共通認識を持つことができるだけでなく、フロントエンドではAPIクライアントまで自動生成可能です。 Relayであれば公式にrelay-compilerが用意されているため、公式が型補完までサポートしてくれています。

ZodとZodios

Zod

zod.dev

Zodは主にバリデーションのために使われるスキーマ定義ライブラリです。

バリデーション文脈で言えば、yupもよく使われています。 またNext.jsは最近v12.3ajvを公式にサポートしています。

Zodは、近年バリデーションだけで使われることを飛び越えて、スキーマ定義を活用したライブラリが数多く開発されてきており、エコシステムか発展しつつあります。

Zodios

github.com

ZodiosはZodのスキーマ定義を活用したAPIクライアントです。 あまりメジャーではありませんが、zodのスキーマ定義をベースにAPIクライアントを動的に生成するものとしては十分なライブラリです。

ただ名前からもわかる通りaxios のインターフェースをベースにしているのですか、最近 axios を使いたいモチベーションが低いので、名前とaxiosが依存に入るのが微妙だなと思っています。

実際のAPI呼び出しのライブラリにはaxiosを利用せず、プラグイン的にfetchを使うことができます。 ただしfetchを利用する場合でもaxiosのI/Fをベースに記述されているためaxiosの依存は必要です。

Zodiosを使う

まずはZodでスキーマ定義をします。 API のレスポンスが以下のような形式だったとします。

type User = {
  id: number;
  name: string;
  email: string;
};

Zodのスキーマ定義は以下のようにします。

const userResponse = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string(),
});

以下のように TypeScript の型を手に入れることができます。

export type User = z.infer<typeof userResponse>;

Zodのよいところはスキーマ定義を行うことで、単純なTypeScriptの型では表現できない制約を正しく表現できることが利点です。 そのため、以下のように書くことで得られる Type は同じですが、ドキュメンテーション的な役割を担うことができます。 

const userResponse = z.object({
  id: z.number().int(),
  name: z.string().min(1).max(1024),
  email: z.string().email();
});

スキーマ定義を元にZodiosのAPI定義を作成します。

export const userApi = zodios
  .apiBuilder({
    method: "get",
    path: "/user/:id",
    alias: "getUser",
    description: "Get a user",
    response: userResponse,
  })
  .addEndpoint({
    method: "post",
    path: "/user/:id",
    alias: "postUser",
    parameters: [
      {
        name: "body",
        description: "User Info",
        type: "Body",
        schema: z.object({
          name: z.string().min(1).max(1024),
          email: z.string().email(),
        }),
      },
    ],
    description: "Create a user",
    response: userResponse,
  })
  .build();

apiBuilderaddEndpointでエンドポイントを定義の生成、追加しています。 エンドポイントごとにaliasで自分の好きなメソッド名を定義できます。 リクエストするクエリやボディに対しても、zodのスキーマ定義を利用できるだけでなく、descriptionを記載しておくことができます。 API定義をimportしてZodiosのクラスを作成します。

const apiClient = new Zodios(API_ENDPOINT, [...userApi]);

実際に利用する際にはalias で書いたメソッド名を利用し、パスを記述する必要はなく、クエリ、レスポンスは型補完が効きます。

const data = await apiClient.getUser({ params: { id: 1 } });

レスポンスの型補完が効いている

さらにZodのスキーマ定義を使ってリクエストやレスポンスに対してバリデーションが効きます。 開発段階では、スキーマ定義通りじゃないリクエストやレスポンスにZodiosのエラーで気づくことができるので有用です。

Zodのバリデーションエラーをthrowできる

Zodios から Open APIをアウトプットすることで、ドキュメントとしても活用できる...と思いましたが、 現時点で適切なライブラリが存在しないようです(残念)。

github.com (空のレポジトリ)

Zod から Open API 自体は可能なので、工夫すればできるかもしれません(やってない)。

www.npmjs.com

この記事のコードサンプルが書かれているレポジトリはこちらです。

github.com

余談: MSW

フロントエンド開発では言わずと知れたモックツールです。 mswのハンドラにはレスポンスを直接型付けできます。 ただこれでは、パスとレスポンスの型は人間が正しく付けてあげる必要があるので、ミスを防げません。 そこでzodios の型を流用してパスとレスポンスの型付けを行うことができます。

export function restGet<Path extends Paths<Api, "get">>(
  path: Path,
  resolver: ResponseResolver<
    RestRequest,
    RestContext,
    Awaited<Response<Api, "get", Path>>
  >
) {
  return rest.get(`${API_ENDPOINT}${path}`, resolver);
}

これにより、パスが補完され、パスに応じたレスポンスの型に補完が効くようになります。

  restGet("/user/:id", (req, res, ctx) => {
    const id = req.params.id;
    const data = users.find((u) => u.id === Number(id));

    if (!data) {
      return res(ctx.status(404));
    }

    return res(ctx.status(200), ctx.json(data));
  }),

takepepeさんの記事を参考にしました。

zenn.dev

またzodiosの型がとてもよく作られているので、そこまで複雑な型パズルをすることなく、型補完が適用できました。

まとめ

ZodとZodiosを利用したAPIクライアントへの型付けについて書きました。 MSWに型をつける方法もついでに紹介しました。

Zodiosは特にファイルを自動生成することなく、割と書き心地よくAPIクライアントを型補完した状態で書き進められるライブラリです。

あまりメジャーではないのですが、僕はだいぶ使い心地が気に入ったので、多くのひとに使ってもらえたらいいなと思います。