OpenAPIからTypeScript型定義とfetchクライアントを自動生成する:openapi-typescript + openapi-fetch
はじめに
OpenAPIでAPI仕様を定義したあと、TypeScript側で同じ型を手書きしていませんか?
openapi-typescript を使うとOpenAPI仕様ファイルからTypeScriptの型定義を自動生成できます。さらに openapi-fetch を組み合わせることで、生成した型を使ったAPIクライアントが型安全に書けます。
バックエンドのAPI仕様が変わったら型を再生成するだけで、TypeScriptのコンパイルエラーとして変更箇所を検知できます。
OpenAPIの基本的な書き方は「OpenAPI入門:YAML仕様の書き方からSwagger UIで確認するまで」を参照してください。
インストール
npm install -D openapi-typescript typescript npm install openapi-fetch
OpenAPI仕様から型を生成する
openapi-typescript コマンドでYAMLファイルから型定義ファイルを生成します。
npx openapi-typescript openapi.yaml -o src/types/api.ts
リモートのURLからも生成できます。
npx openapi-typescript https://api.example.com/openapi.yaml -o src/types/api.ts
生成される型定義の例
以下のOpenAPI仕様があるとします。
openapi: 3.1.0 info: title: User API version: 1.0.0 paths: /users: get: responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/User' post: requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateUserInput' responses: '201': content: application/json: schema: $ref: '#/components/schemas/User' /users/{id}: get: parameters: - name: id in: path required: true schema: type: integer responses: '200': content: application/json: schema: $ref: '#/components/schemas/User' '404': content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' components: schemas: User: type: object required: [id, name, email] properties: id: type: integer name: type: string email: type: string CreateUserInput: type: object required: [name, email] properties: name: type: string email: type: string ErrorResponse: type: object properties: code: type: string message: type: string
生成される src/types/api.ts はこのような構造になります。
export interface paths { "/users": { get: { responses: { 200: { content: { "application/json": components["schemas"]["User"][]; }; }; }; }; post: { requestBody: { content: { "application/json": components["schemas"]["CreateUserInput"]; }; }; responses: { 201: { content: { "application/json": components["schemas"]["User"]; }; }; }; }; }; "/users/{id}": { get: { parameters: { path: { id: number }; }; responses: { 200: { content: { "application/json": components["schemas"]["User"]; }; }; 404: { content: { "application/json": components["schemas"]["ErrorResponse"]; }; }; }; }; }; } export interface components { schemas: { User: { id: number; name: string; email: string; }; CreateUserInput: { name: string; email: string; }; ErrorResponse: { code?: string; message?: string; }; }; }
openapi-fetchでAPIを呼び出す
生成した型定義を使ってAPIクライアントを作成します。
import createClient from "openapi-fetch"; import type { paths } from "./types/api"; const client = createClient<paths>({ baseUrl: "https://api.example.com", });
GETリクエスト
// ユーザー一覧取得 const { data, error } = await client.GET("/users"); // data は User[] 型、error は ErrorResponse 型として推論される if (error) { console.error(error.message); } // ユーザー1件取得(パスパラメータ) const { data: user, error: userError } = await client.GET("/users/{id}", { params: { path: { id: 1 }, }, }); // user は User 型として推論される
パスパラメータの型が合わない場合はコンパイルエラーになります。
// エラー: id は number 型なのに string を渡している await client.GET("/users/{id}", { params: { path: { id: "abc" }, // Type 'string' is not assignable to type 'number' }, });
POSTリクエスト
const { data: newUser, error } = await client.POST("/users", { body: { name: "田中太郎", email: "tanaka@example.com", }, }); // newUser は User 型として推論される
リクエストボディに必須フィールドが足りない場合もコンパイルエラーになります。
// エラー: email が required なのに省略している await client.POST("/users", { body: { name: "田中太郎", // Property 'email' is missing }, });
クエリパラメータ
await client.GET("/users", { params: { query: { page: 1, per_page: 20, }, }, });
APIクライアントをモジュール化する
プロジェクトで使いやすいようにクライアントをモジュールにまとめます。
// src/lib/api.ts import createClient from "openapi-fetch"; import type { paths } from "../types/api"; export const apiClient = createClient<paths>({ baseUrl: process.env.NEXT_PUBLIC_API_BASE_URL ?? "https://api.example.com", headers: { "Content-Type": "application/json", }, });
// src/api/users.ts import { apiClient } from "../lib/api"; import type { components } from "../types/api"; type User = components["schemas"]["User"]; type CreateUserInput = components["schemas"]["CreateUserInput"]; export async function getUsers(): Promise<User[]> { const { data, error } = await apiClient.GET("/users"); if (error) throw new Error(error.message); return data; } export async function getUser(id: number): Promise<User> { const { data, error } = await apiClient.GET("/users/{id}", { params: { path: { id } }, }); if (error) throw new Error(error.message); return data; } export async function createUser(input: CreateUserInput): Promise<User> { const { data, error } = await apiClient.POST("/users", { body: input }); if (error) throw new Error(error.message); return data; }
npm scriptsに組み込む
package.json のscriptsに登録しておくと、仕様変更のたびに手軽に再生成できます。
{ "scripts": { "generate:api": "openapi-typescript openapi.yaml -o src/types/api.ts" } }
npm run generate:api
CI上で自動生成することで、仕様と型定義の乖離を防げます。
まとめ
openapi-typescriptでOpenAPI仕様からTypeScript型定義を自動生成できるopenapi-fetchで生成した型を使った型安全なAPIクライアントが書ける- パスパラメータ・リクエストボディ・レスポンスがすべてOpenAPI仕様の型として推論される
- API仕様が変わったら
npm run generate:apiで再生成するだけでよい
ReactプロジェクトでReact Queryフックまで自動生成したい場合は「orvalでOpenAPIからReact Queryフックを自動生成する」を参照してください。
スキーマのnullable・enum・共通化については「OpenAPIスキーマ設定チートシート:nullable・enum・共通化まで」を参照してください。