English article is here: openapi-fetch-gen – Generate TypeScript API client from OpenAPI TypeScript interface definitions created by openapi-typescript - DEV Community
npm registryにも公開されています。
従って以下のようにダウンロード可能です。
npm install @moznion/openapi-fetch-gen
これは何 / 背景
OpenAPI 3の仕様からopenapi-ts/openapi-typescriptにより生成されたスキーマ (.d.ts) ファイルからTypeScriptのAPIクライアントを自動生成するツールです。
openapi-ts/openapi-typescriptはかなりパワフルかつ完成度が高いツールで、比較的複雑なOpenAPIの定義からでも綺麗なスキーマファイルを生成することができます。なおかつopenapi-fetchというライブラリも併せて提供されており、これを用いることで生成されたスキーマファイルと組み合わせてAPI Clientをtype-safeな形で作成することができます。
ただこれには1点課題があり、openapi-fetchはあくまで「スキーマを読み込んでAPI Clientが作れるライブラリ」であり「APIクライアントを自動生成するものではない」ため、各エンドポイントについてAPIクライアントをマニュアルで作成する必要がありました。
という課題を解決するためにこの度作成されたのがこのopenapi-fetch-genです。
用法
以下のようなOpenAPI 3の定義YAMLを例に取ります。
schema.yaml (ちょっと長いですが、まあよくあるREST APIの定義だと思ってください)
openapi: 3.0.3
info:
title: Fictional Library User Management Service API
version: 1.0.0
description: |
A RESTful API for managing library user records and their loan information.
servers:
- url: https://api.fictionallibrary.example.com/v1
description: Production server
paths:
/users/{userId}:
parameters:
- $ref: '#/components/parameters/userId'
- name: Authorization
in: header
required: true
schema:
type: string
description: Authorization Header
- name: Application-Version
in: header
required: true
schema:
type: string
description: Application version
- name: Something-Id
in: header
required: true
schema:
type: string
description: Identifier of something
get:
summary: Get user details
description: Retrieve detailed information for a specific user.
responses:
'200':
description: User details
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
put:
summary: Replace user
description: Replace a user's entire record.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserUpdate'
responses:
'200':
description: Updated user record
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
patch:
summary: Update user fields
description: Partially update a user's information.
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserPatch'
responses:
'200':
description: Updated user record
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
delete:
summary: Delete user
description: Soft-delete a user record.
responses:
'204':
description: User deleted (no content)
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
components:
parameters:
userId:
name: userId
in: path
required: true
description: Unique user identifier (UUID)
schema:
type: string
format: uuid
responses:
BadRequest:
description: Bad request due to invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Unauthorized:
description: Authentication required or failed
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Forbidden:
description: Insufficient permissions to access resource
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Conflict:
description: Conflict with current state of the resource
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
schemas:
Error:
type: object
properties:
code:
type: integer
description: HTTP status code
message:
type: string
description: Error message detailing the cause
required:
- code
- message
User:
type: object
properties:
id:
type: string
format: uuid
name:
type: string
email:
type: string
format: email
membershipType:
type: string
enum: [REGULAR, PREMIUM, STUDENT]
registeredAt:
type: string
format: date-time
address:
$ref: '#/components/schemas/Address'
required:
- id
- name
- email
- membershipType
- registeredAt
Address:
type: object
properties:
postalCode:
type: string
street:
type: string
city:
type: string
country:
type: string
required:
- street
- city
- country
UserCreate:
type: object
properties:
name:
type: string
email:
type: string
format: email
membershipType:
type: string
enum: [REGULAR, PREMIUM, STUDENT]
address:
$ref: '#/components/schemas/Address'
required:
- name
- email
- membershipType
UserUpdate:
allOf:
- $ref: '#/components/schemas/UserCreate'
- type: object
properties:
id:
type: string
format: uuid
required:
- id
UserPatch:
type: object
description: Schema for partial updates – include only fields to change
properties:
name:
type: string
email:
type: string
format: email
membershipType:
type: string
enum: [REGULAR, PREMIUM, STUDENT]
address:
$ref: '#/components/schemas/Address'
この時に以下のようなコマンドを実行するとopenapi-typescriptを用いてスキーマファイル (.d.ts) が生成されます:
openapi-typescript --output schema.d.ts ./schema.yaml
この時に生成されるスキーマファイルは以下のようになっています
schema.d.ts
export interface paths {
"/users/{userId}": {
parameters: {
query?: never;
header: {
@description
Authorization: string;
@description
"Application-Version": string;
@description
"Something-Id": string;
};
path: {
@description
userId: components["parameters"]["userId"];
};
cookie?: never;
};
@description
get: {
parameters: {
query?: never;
header: {
@description
Authorization: string;
@description
"Application-Version": string;
@description
"Something-Id": string;
};
path: {
@description
userId: components["parameters"]["userId"];
};
cookie?: never;
};
requestBody?: never;
responses: {
@description
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["User"];
};
};
400: components["responses"]["BadRequest"];
401: components["responses"]["Unauthorized"];
403: components["responses"]["Forbidden"];
404: components["responses"]["NotFound"];
};
};
@description
put: {
parameters: {
query?: never;
header: {
@description
Authorization: string;
@description
"Application-Version": string;
@description
"Something-Id": string;
};
path: {
@description
userId: components["parameters"]["userId"];
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["UserUpdate"];
};
};
responses: {
@description
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["User"];
};
};
400: components["responses"]["BadRequest"];
401: components["responses"]["Unauthorized"];
403: components["responses"]["Forbidden"];
404: components["responses"]["NotFound"];
};
};
post?: never;
@description
delete: {
parameters: {
query?: never;
header: {
@description
Authorization: string;
@description
"Application-Version": string;
@description
"Something-Id": string;
};
path: {
@description
userId: components["parameters"]["userId"];
};
cookie?: never;
};
requestBody?: never;
responses: {
@description
204: {
headers: {
[name: string]: unknown;
};
content?: never;
};
400: components["responses"]["BadRequest"];
401: components["responses"]["Unauthorized"];
403: components["responses"]["Forbidden"];
404: components["responses"]["NotFound"];
};
};
options?: never;
head?: never;
@description
patch: {
parameters: {
query?: never;
header: {
@description
Authorization: string;
@description
"Application-Version": string;
@description
"Something-Id": string;
};
path: {
@description
userId: components["parameters"]["userId"];
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["UserPatch"];
};
};
responses: {
@description
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["User"];
};
};
400: components["responses"]["BadRequest"];
401: components["responses"]["Unauthorized"];
403: components["responses"]["Forbidden"];
404: components["responses"]["NotFound"];
};
};
trace?: never;
};
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
Error: {
@description
code: number;
@description
message: string;
};
User: {
id: string;
name: string;
email: string;
@enum{string}
membershipType: "REGULAR" | "PREMIUM" | "STUDENT";
registeredAt: string;
address?: components["schemas"]["Address"];
};
Address: {
postalCode?: string;
street: string;
city: string;
country: string;
};
UserCreate: {
name: string;
email: string;
@enum{string}
membershipType: "REGULAR" | "PREMIUM" | "STUDENT";
address?: components["schemas"]["Address"];
};
UserUpdate: components["schemas"]["UserCreate"] & {
id: string;
};
@description
UserPatch: {
name?: string;
email?: string;
@enum{string}
membershipType?: "REGULAR" | "PREMIUM" | "STUDENT";
address?: components["schemas"]["Address"];
};
};
responses: {
@description
BadRequest: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
@description
Unauthorized: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
@description
Forbidden: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
@description
NotFound: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
@description
Conflict: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
};
parameters: {
@description
userId: string;
};
requestBodies: never;
headers: never;
pathItems: never;
}
export type $defs = Record<string, never>;
export type operations = Record<string, never>;
これまでは、この生成されたスキーマファイルとopenapi-fetchを併用して手でAPIクライアントを書く必要がありましたが、openapi-fetch-genを使うとスキーマファイルを入力として以下のように実行することでAPIクライアントを自動生成することができます:
openapi-fetch-gen --input ./schema.d.ts --output ./generated_client.ts
generated_client.tsはschema.d.tsを元に自動生成されたTypeScriptレベルでtype-safeなAPIクライアントです。一部を抽出すると以下のようなコードとなります:
import createClient, { type ClientOptions } from "openapi-fetch";
import type { paths } from "./schema"; // generated by openapi-typescript
export class Client {
private readonly client;
constructor(clientOptions: ClientOptions) {
this.client = createClient<paths>(clientOptions);
}
...
async putUsersUserid(
params: {
header: {
Authorization: string;
"Application-Version": string;
"Something-Id": string;
};
path: { userId: string };
},
body: {
name: string;
email: string;
membershipType: "REGULAR" | "PREMIUM" | "STUDENT";
address?: {
postalCode?: string;
street: string;
city: string;
country: string;
};
} & { id: string },
) {
return await this.client.PUT("/users/{userId}", {
params,
body,
});
}
...
}
FYI: 生成されるAPIクライアント (フル)
import createClient, { type ClientOptions } from "openapi-fetch";
import type { paths } from "./schema"; // generated by openapi-typescript
export class Client {
private readonly client;
constructor(clientOptions: ClientOptions) {
this.client = createClient<paths>(clientOptions);
}
async getUsersUserid(params: {
header: {
Authorization: string;
"Application-Version": string;
"Something-Id": string;
};
path: { userId: string };
}) {
return await this.client.GET("/users/{userId}", {
params,
});
}
async putUsersUserid(
params: {
header: {
Authorization: string;
"Application-Version": string;
"Something-Id": string;
};
path: { userId: string };
},
body: {
name: string;
email: string;
membershipType: "REGULAR" | "PREMIUM" | "STUDENT";
address?: {
postalCode?: string;
street: string;
city: string;
country: string;
};
} & { id: string },
) {
return await this.client.PUT("/users/{userId}", {
params,
body,
});
}
async deleteUsersUserid(params: {
header: {
Authorization: string;
"Application-Version": string;
"Something-Id": string;
};
path: { userId: string };
}) {
return await this.client.DELETE("/users/{userId}", {
params,
});
}
async patchUsersUserid(
params: {
header: {
Authorization: string;
"Application-Version": string;
"Something-Id": string;
};
path: { userId: string };
},
body: {
name?: string;
email?: string;
membershipType?: "REGULAR" | "PREMIUM" | "STUDENT";
address?: {
postalCode?: string;
street: string;
city: string;
country: string;
};
},
) {
return await this.client.PATCH("/users/{userId}", {
params,
body,
});
}
}
なかなか便利なのではないでしょうか。
中身としてはどうなっているのか
TypeScript Compiler APIをdsherret/ts-morphライブラリを介して利用することで d.ts ファイルを解釈し、その内容・構造に基いてAPIクライアントコードを生成しています。また、上記の例のように生成されるAPIクライアントのコードの中ではopenapi-fetchが用いられています。
まとめ
というわけで、openapi-typescriptが出力するTypeScriptのスキーマからTypeScriptのAPIクライアントを自動生成するツールであるopenapi-fetch-genのご紹介でした。どうぞご利用ください。
ところで、これに加えて "Default HTTP Header" 機能というものも用意しており、これは本来の生成クライアントコードが要求してくる「APIエンドポイントを呼び出すメソッドそれぞれに明示的にHTTP Headerを渡さなければならない *1」というものを省力化するものです。良くある例としては、 Authorization ヘッダなんかはほとんどのエンドポイントで共通して渡す必要があったりしますが、これをすべてのAPIメソッド呼び出しメソッドで渡さなければならないとなると面倒ですよね。というわけでopenapi-fetch-genではジェネリクスを活用することでデフォルトのヘッダを渡せるようにしており、加えてちょっとしたTypeScriptの型のテクニックを内部的に使うことで、デフォルトヘッダとして指定されたヘッダを各API呼び出しメソッドで省略できるようにしています。
興味のある方はDefault HTTP Headersのセクションをご参照ください。