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
/** * This file was auto-generated by openapi-typescript. * Do not make direct changes to the file. */ export interface paths { "/users/{userId}": { parameters: { query?: never; header: { /** @description Authorization Header */ Authorization: string; /** @description Application version */ "Application-Version": string; /** @description Identifier of something */ "Something-Id": string; }; path: { /** @description Unique user identifier (UUID) */ userId: components["parameters"]["userId"]; }; cookie?: never; }; /** * Get user details * @description Retrieve detailed information for a specific user. */ get: { parameters: { query?: never; header: { /** @description Authorization Header */ Authorization: string; /** @description Application version */ "Application-Version": string; /** @description Identifier of something */ "Something-Id": string; }; path: { /** @description Unique user identifier (UUID) */ userId: components["parameters"]["userId"]; }; cookie?: never; }; requestBody?: never; responses: { /** @description User details */ 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"]; }; }; /** * Replace user * @description Replace a user's entire record. */ put: { parameters: { query?: never; header: { /** @description Authorization Header */ Authorization: string; /** @description Application version */ "Application-Version": string; /** @description Identifier of something */ "Something-Id": string; }; path: { /** @description Unique user identifier (UUID) */ userId: components["parameters"]["userId"]; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UserUpdate"]; }; }; responses: { /** @description Updated user record */ 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; /** * Delete user * @description Soft-delete a user record. */ delete: { parameters: { query?: never; header: { /** @description Authorization Header */ Authorization: string; /** @description Application version */ "Application-Version": string; /** @description Identifier of something */ "Something-Id": string; }; path: { /** @description Unique user identifier (UUID) */ userId: components["parameters"]["userId"]; }; cookie?: never; }; requestBody?: never; responses: { /** @description User deleted (no content) */ 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; /** * Update user fields * @description Partially update a user's information. */ patch: { parameters: { query?: never; header: { /** @description Authorization Header */ Authorization: string; /** @description Application version */ "Application-Version": string; /** @description Identifier of something */ "Something-Id": string; }; path: { /** @description Unique user identifier (UUID) */ userId: components["parameters"]["userId"]; }; cookie?: never; }; requestBody: { content: { "application/json": components["schemas"]["UserPatch"]; }; }; responses: { /** @description Updated user record */ 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 HTTP status code */ code: number; /** @description Error message detailing the cause */ message: string; }; User: { /** Format: uuid */ id: string; name: string; /** Format: email */ email: string; /** @enum {string} */ membershipType: "REGULAR" | "PREMIUM" | "STUDENT"; /** Format: date-time */ registeredAt: string; address?: components["schemas"]["Address"]; }; Address: { postalCode?: string; street: string; city: string; country: string; }; UserCreate: { name: string; /** Format: email */ email: string; /** @enum {string} */ membershipType: "REGULAR" | "PREMIUM" | "STUDENT"; address?: components["schemas"]["Address"]; }; UserUpdate: components["schemas"]["UserCreate"] & { /** Format: uuid */ id: string; }; /** @description Schema for partial updates – include only fields to change */ UserPatch: { name?: string; /** Format: email */ email?: string; /** @enum {string} */ membershipType?: "REGULAR" | "PREMIUM" | "STUDENT"; address?: components["schemas"]["Address"]; }; }; responses: { /** @description Bad request due to invalid input */ BadRequest: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["Error"]; }; }; /** @description Authentication required or failed */ Unauthorized: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["Error"]; }; }; /** @description Insufficient permissions to access resource */ Forbidden: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["Error"]; }; }; /** @description Resource not found */ NotFound: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["Error"]; }; }; /** @description Conflict with current state of the resource */ Conflict: { headers: { [name: string]: unknown; }; content: { "application/json": components["schemas"]["Error"]; }; }; }; parameters: { /** @description Unique user identifier (UUID) */ 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); } ... /** * Replace user */ 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クライアント (フル)
// THIS FILE IS AUTO-GENERATED BY openapi-fetch-gen. // DO NOT EDIT THIS FILE MANUALLY. // See Also: https://github.com/moznion/openapi-fetch-gen 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); } /** * Get user details */ async getUsersUserid(params: { header: { Authorization: string; "Application-Version": string; "Something-Id": string; }; path: { userId: string }; }) { return await this.client.GET("/users/{userId}", { params, }); } /** * Replace user */ 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, }); } /** * Delete user */ async deleteUsersUserid(params: { header: { Authorization: string; "Application-Version": string; "Something-Id": string; }; path: { userId: string }; }) { return await this.client.DELETE("/users/{userId}", { params, }); } /** * Update user fields */ 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のセクションをご参照ください。
*1:上記の生成コード例におけるメソッドの引数中のparams.headerがそれに当たります