OpenAPI → Typescript Client ジェネレーター書き比べ 2024
モチベーション
React + Typescript でアプリケーションを開発します。この時、API 定義は OpenAPI 形式で提供されています。
ジェネレーターで機械的にクライアント用のコードを生成することで、型安全でミスが入り込みにくいコードにしたいです。
実現できそうなライブラリはいくつもあるので、実際に書き比べもしながら比較します。
検討ポイント
- 最近の流行りはどうか
- メンテナンス状況はどうか
- 変換したい 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 にあります。以下では必要な箇所のみ抜粋して掲載します。
インストール
インストールコマンドとインストール時点でのバージョンを記載します。
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
% npm install --save-dev @openapitools/openapi-generator-cli
- "@openapitools/openapi-generator-cli": "2.15.3"
@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/api.yaml -o ./src/openapi-typescript-client/schema.d.ts
@openapitools/openapi-generator-cli
ローカルで動作させるには Java が必要なようで、回避のため Docker で起動するようにしました。
"useDocker": true
とした設定ファイル 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"
}
}
}
}
openapi-generator-cli generate
@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
tastiness:
type: integer
description: おいしさスコア
example: 90
nullable: true
/**
* @description おいしさスコア
* @example 90
*/
tastiness?: number | null;
/**
* おいしさスコア
* @type {number}
* @memberof Mushroom
*/
tastiness?: number | null;
/**
* おいしさスコア
*/
tastiness?: (number) | null;
readOnly
id:
type: string
format: uuid
readOnly: true
description: きのこID
/**
* Format: uuid
* @description きのこID
*/
readonly id: string;
/**
* きのこID
* @type {string}
* @memberof Mushroom
*/
readonly id: string;
/**
* きのこID
*/
readonly id: string;
allOf
allOf:
- type: object
required:
- results
properties:
results:
type: array
items:
$ref: "#/components/schemas/Mushroom"
- $ref: "#/components/schemas/PaginationParts"
"application/json": {
results: components["schemas"]["Mushroom"][];
} & components["schemas"]["PaginationParts"];
{
/**
* 総数
* @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>;
}
({ results: Array<Mushroom> } & PaginationParts)
oneOf
oneOf:
- $ref: "#/components/schemas/Http422UnprocessableEntity"
- $ref: "#/components/schemas/Http422InvalidRequestBody"
"application/json": components["schemas"]["Http422UnprocessableEntity"] | components["schemas"]["Http422InvalidRequestBody"];
Http422InvalidRequestBody | Http422UnprocessableEntity;
(Http422UnprocessableEntity | Http422InvalidRequestBody)
enum
size:
type: string
description: 大きさの種類
enum: [small, medium, large]
/**
* @description 大きさの種類
* @enum {string}
*/
size: "small" | "medium" | "large";
/**
* 大きさの種類
* @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];
/**
* 大きさの種類
*/
size: 'small' | 'medium' | 'large';
/**
* 大きさの種類
*/
export type size = 'small' | 'medium' | 'large';
*
key の Http422InvalidRequestBody:
type: object
properties:
"*":
type: array
items:
type: string
example: この項目は必須です。
code:
type: object
properties:
"*":
type: array
items:
type: string
example: required
Http422InvalidRequestBody: {
"*"?: string[];
code?: {
"*"?: string[];
};
};
/**
*
* @export
* @interface Http422InvalidRequestBodyCode
*/
export interface Http422InvalidRequestBodyCode {
/**
*
* @type {Array<string>}
* @memberof Http422InvalidRequestBodyCode
*/
?: Array<string>;
}
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 の *
」の変換で *
が解釈できずに変換できませんでした。
それ以外の部分は問題なく変換されました。少しずつ思想の違いが出ていておもしろいです。
以下の順で記述が簡潔だと感じました。
- @hey-api/openapi-ts
- openapi-typescript
- 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 呼び出しの比較
基本設定
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}}",
},
});
import { client } from "./hey-api-openapi-ts-client/sdk.gen";
client.setConfig({
baseUrl: "https://api.example.com",
headers: {
"X-CSRFToken": "{{My Token}}",
},
});
GET (Query あり)
await client.GET("/mushrooms/", {
params: {
query: {
limit: 10,
offset: 5,
},
},
});
await getMushrooms({
query: {
limit: 10,
offset: 5,
},
});
GET (path パラメータあり)
await client.GET("/mushrooms/{mushroom_id}/", {
params: {
path: {
mushroom_id: "52596577-f76f-4777-a569-32b24631a8e8",
},
},
});
await getMushroomsByMushroomId({
path: {
mushroom_id: "52596577-f76f-4777-a569-32b24631a8e8",
},
});
POST
await client.POST("/mushrooms/", {
body: {
name: "しめじ",
description: "一般的な食用きのこ",
orderable: true,
size: "medium",
} as components["schemas"]["Mushroom"],
});
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