🍄

OpenAPI → Typescript Client ジェネレーター書き比べ 2024

2024/12/14に公開

モチベーション

React + Typescript でアプリケーションを開発します。この時、API 定義は OpenAPI 形式で提供されています。
ジェネレーターで機械的にクライアント用のコードを生成することで、型安全でミスが入り込みにくいコードにしたいです。
実現できそうなライブラリはいくつもあるので、実際に書き比べもしながら比較します。

検討ポイント

  • 最近の流行りはどうか
  • メンテナンス状況はどうか
  • 変換したい API 定義に対応できるか
  • クライアントのコードは書きやすいか

最近の流行りはどうか

https://npmtrends.com/@openapitools/openapi-generator-cli-vs-openapi-typescript-vs-openapi-typescript-codegen-vs-orval-vs-swagger-typescript-api

(2024 年 12 月 12 日時点)

openapi-typescript が勢いを増しています。
@openapitools/openapi-generator-cli が次点で、その下はお団子状態です。

メンテナンス状況はどうか

上と同じく npm-trends から。

比較的どれもアクティブにメンテナンスされています。
openapi-typescript-codegen は開発が終了していますが、公式が @hey-api/openapi-ts を今後使うようアナウンスしています。

@hey-api/openapi-ts は新しいので利用者は少なそうですが、大変アクティブに開発が進められています。

書き比べるライブラリ

トレンドから、以下の3ライブラリを書き比べてみます。

コード全体は Github にあります。以下では必要な箇所のみ抜粋して掲載します。
https://github.com/chocolat0w0/compare-ts-api-libraries

インストール

インストールコマンドとインストール時点でのバージョンを記載します。

openapi-typescript

openapi-typescript
% npm install --save-dev openapi-typescript
% npm install openapi-fetch
  • "openapi-typescript": "7.4.4"
  • "openapi-fetch": "0.13.3"

@openapitools/openapi-generator-cli

@openapitools/openapi-generator-cli
% npm install --save-dev @openapitools/openapi-generator-cli
  • "@openapitools/openapi-generator-cli": "2.15.3"

@hey-api/openapi-ts

@hey-api/openapi-ts
% npm install --save-dev @hey-api/openapi-ts
% npm install @hey-api/client-fetch
  • "@hey-api/openapi-ts": "^0.59.2"
  • "@hey-api/client-fetch": "0.5.4"

変換コマンド

openapi-typescript

openapi-typescript
openapi-typescript ./openapi/api.yaml -o ./src/openapi-typescript-client/schema.d.ts

@openapitools/openapi-generator-cli

ローカルで動作させるには Java が必要なようで、回避のため Docker で起動するようにしました。
"useDocker": true とした設定ファイル openapitools.json を用意します。入出力ファイルの設定もそちらに記述しています。

openapitools.json
{
  "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json",
  "spaces": 2,
  "generator-cli": {
    "version": "7.10.0",
    "useDocker": true,
    "generators": {
      "v3.0": {
        "generatorName": "typescript-fetch",
        "inputSpec": "./openapi/api.yaml",
        "output": "./src/openapi-generator-cli-client"
      }
    }
  }
}
@openapitools/openapi-generator-cli
openapi-generator-cli generate

@hey-api/openapi-ts

@hey-api/openapi-ts
openapi-ts -i ./openapi/api.yaml -o ./src/hey-api-openapi-ts-client -c @hey-api/client-fetch

変換したい API 定義に対応できるか

nullable, readOnly, allOf, oneOf, enum, key の * の変換結果を比べます。

nullable

api.yaml
tastiness:
 type: integer
 description: おいしさスコア
 example: 90
 nullable: true
openapi-typescript
/**
 * @description おいしさスコア
 * @example 90
 */
tastiness?: number | null;
@openapitools/openapi-generator-cli
/**
 * おいしさスコア
 * @type {number}
 * @memberof Mushroom
 */
tastiness?: number | null;
@hey-api/openapi-ts
/**
 * おいしさスコア
 */
tastiness?: (number) | null;

readOnly

api.yaml
id:
  type: string
  format: uuid
  readOnly: true
  description: きのこID
openapi-typescript
/**
 * Format: uuid
 * @description きのこID
 */
readonly id: string;
@openapitools/openapi-generator-cli
/**
 * きのこID
 * @type {string}
 * @memberof Mushroom
 */
readonly id: string;
@hey-api/openapi-ts
/**
 * きのこID
 */
readonly id: string;

allOf

api.yaml
allOf:
  - type: object
    required:
      - results
    properties:
      results:
        type: array
        items:
          $ref: "#/components/schemas/Mushroom"
  - $ref: "#/components/schemas/PaginationParts"
openapi-typescript
"application/json": {
    results: components["schemas"]["Mushroom"][];
} & components["schemas"]["PaginationParts"];
@openapitools/openapi-generator-cli
{
    /**
     * 総数
     * @type {number}
     * @memberof MushroomsGet200Response
     */
    count: number;
    /**
     * 次のページへのURL
     * @type {string}
     * @memberof MushroomsGet200Response
     */
    next: string | null;
    /**
     * 前のページへのURL
     * @type {string}
     * @memberof MushroomsGet200Response
     */
    previous: string | null;
    /**
     *
     * @type {Array<Mushroom>}
     * @memberof MushroomsGet200Response
     */
    results: Array<Mushroom>;
}
@hey-api/openapi-ts
({ results: Array<Mushroom> } & PaginationParts)

oneOf

api.yaml
oneOf:
  - $ref: "#/components/schemas/Http422UnprocessableEntity"
  - $ref: "#/components/schemas/Http422InvalidRequestBody"
openapi-typescript
"application/json": components["schemas"]["Http422UnprocessableEntity"] | components["schemas"]["Http422InvalidRequestBody"];
@openapitools/openapi-generator-cli
Http422InvalidRequestBody | Http422UnprocessableEntity;
@hey-api/openapi-ts
(Http422UnprocessableEntity | Http422InvalidRequestBody)

enum

api.yaml
size:
  type: string
  description: 大きさの種類
  enum: [small, medium, large]
openapi-typescript
/**
 * @description 大きさの種類
 * @enum {string}
 */
size: "small" | "medium" | "large";
@openapitools/openapi-generator-cli
/**
 * 大きさの種類
 * @type {string}
 * @memberof Mushroom
 */
size: MushroomSizeEnum;

export const MushroomSizeEnum = {
    Small: 'small',
    Medium: 'medium',
    Large: 'large'
} as const;
export type MushroomSizeEnum = typeof MushroomSizeEnum[keyof typeof MushroomSizeEnum];
@hey-api/openapi-ts
/**
 * 大きさの種類
 */
size: 'small' | 'medium' | 'large';

/**
 * 大きさの種類
 */
export type size = 'small' | 'medium' | 'large';

key の *

api.yaml
Http422InvalidRequestBody:
  type: object
  properties:
    "*":
      type: array
      items:
        type: string
        example: この項目は必須です。
    code:
      type: object
      properties:
        "*":
          type: array
          items:
            type: string
            example: required

openapi-typescript
Http422InvalidRequestBody: {
    "*"?: string[];
    code?: {
        "*"?: string[];
    };
};
@openapitools/openapi-generator-cli
/**
 *
 * @export
 * @interface Http422InvalidRequestBodyCode
 */
export interface Http422InvalidRequestBodyCode {
    /**
     *
     * @type {Array<string>}
     * @memberof Http422InvalidRequestBodyCode
     */
    ?: Array<string>;
}
@hey-api/openapi-ts
export type Http422InvalidRequestBody = {
    '*'?: Array<(string)>;
    code?: {
        '*'?: Array<(string)>;
    };
};

変換結果の小まとめ

項目 openapi-typescript @openapitools/openapi-generator-cli @hey-api/openapi-ts
nullable | null | null | null
readOnly readonly readonly readonly
allOf & 全要素を展開した新しい型を生成 &
oneOf | | |
enum | 定数オブジェクトを生成した上でリテラル型 |
key の * '*'? 変換不可 '*'?

openapi-generator-cli は「key の *」の変換で * が解釈できずに変換できませんでした。

それ以外の部分は問題なく変換されました。少しずつ思想の違いが出ていておもしろいです。
以下の順で記述が簡潔だと感じました。

  1. @hey-api/openapi-ts
  2. openapi-typescript
  3. openapi-generator-cli

@hey-api/openapi-ts は必要最低限をシンプルに定義しています。

openapi-typescript もほぼ同じようなロジックですが、スキーマの型指定が長いです。

openapi-generator-cli は Enum を丁寧に出してくれているのは好感。

この後 API 呼び出し部を書いてみようと思います。
penapi-generator-cli は変換できない部分があったので、openapi-typescript、@hey-api/openapi-ts の2つで書き比べます。

API 呼び出しの比較

基本設定

openapi-typescript
import createClient from "openapi-fetch";
import { components, paths } from "./openapi-typescript-client/schema";

const client = createClient<paths>({
  baseUrl: "https://api.example.com",
  headers: {
    "X-CSRFToken": "{{My Token}}",
  },
});
@hey-api/openapi-ts
import { client } from "./hey-api-openapi-ts-client/sdk.gen";

client.setConfig({
  baseUrl: "https://api.example.com",
  headers: {
    "X-CSRFToken": "{{My Token}}",
  },
});

GET (Query あり)

openapi-typescript
await client.GET("/mushrooms/", {
  params: {
    query: {
      limit: 10,
      offset: 5,
    },
  },
});
@hey-api/openapi-ts
await getMushrooms({
  query: {
    limit: 10,
    offset: 5,
  },
});

GET (path パラメータあり)

openapi-typescript
await client.GET("/mushrooms/{mushroom_id}/", {
  params: {
    path: {
      mushroom_id: "52596577-f76f-4777-a569-32b24631a8e8",
    },
  },
});
@hey-api/openapi-ts
await getMushroomsByMushroomId({
  path: {
    mushroom_id: "52596577-f76f-4777-a569-32b24631a8e8",
  },
});

POST

openapi-typescript
await client.POST("/mushrooms/", {
  body: {
    name: "しめじ",
    description: "一般的な食用きのこ",
    orderable: true,
    size: "medium",
  } as components["schemas"]["Mushroom"],
});
@hey-api/openapi-ts
await postMushrooms({
  body: {
    name: "しめじ",
    description: "一般的な食用きのこ",
    orderable: true,
    size: "medium",
  } as Mushroom,
});

API 呼び出しの比較の小まとめ

openapi-typescript の方が API 定義に実直で、パスやパラメータ構造がそのまま見えています。バックエンドの人と会話する時にはコードを見せたらそのままわかってくれそう。

@hey-api/openapi-ts は関数名や型名をラップして記述量を減らしている感じです。

ちなみに、POST 時にどちらも as で型指定しています。
schemas.Mushroom.id は readOnly なので POST では不要なパラメータです。が、どちらのライブラリもリクエストパラメータの型が schemas.Mushroom であると変換されます。このため、Typescript 的には id が不足していると言われてしまいます。
POST では id を Omit した型で生成してくれると助かるんですが・・・。

まとめ

薄い感じの openapi-typescript か、簡潔に書ける @hey-api/openapi-ts か、どちらを選ぶかは好みかなと思いました。

個人的にはですが、@hey-api/openapi-ts を気に入っています。ガンガン開発しているのでどこかで破壊的変更が入るかもという懸念はありますが、応援していますー。

1ライブラリずつのレビュー記事はいろいろあったのですが、同じコードで比較した記事が見つからなかったので書き比べでした。

Discussion