OpenAPIからTypeScript型定義とfetchクライアントを自動生成する:openapi-typescript + openapi-fetch

スポンサーリンク

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・共通化まで」を参照してください。