FastAPI × @hey-api/openapi-tsで型定義を自動生成する

FastAPI × @hey-api/openapi-tsで型定義を自動生成する

2025.09.01

こんにちは!新規事業統括部のこーすけです。
今回は @hey-api/openapi-ts を使ってみたのでその備忘録になります。

モチベーションとしては、バックエンド:FastAPI、フロントエンド:React+Typescriptで開発を行っており、FastAPIが出力するOpenAPIスキーマからフロントエンドで使用する型定義を自動生成したいといったところです。

前提知識

OpenAPIとは

HTTP API を記述するための標準仕様で、エンドポイントやリクエスト・レスポンスの形式を統一的に表現できます。これにより、APIドキュメントの自動生成や、エンジニア間の知識の共有が容易になり、効率よく開発ができるようになります。

FastAPIとは

Python製の高速な Web フレームワークで、Python標準の型ヒントを活用した自動バリデーションや自動ドキュメント生成が強みです。開発したAPIのエンドポイントや入出力といった仕様をドキュメントに書き起こさなくてよいのは非常に便利です。OpenAPIに準拠したAPIスキーマファイルを生成できます。

@hey-api/openapi-tsとは

OpenAPIのスキーマからTypeScriptの型やクライアントコードを自動生成するライブラリです。自動生成ライブラリにはほかにもいくつかありますが、比較的新しく、開発が活発なライブラリです。MITライセンスのため無料で使用可能です。
https://github.com/hey-api/openapi-ts

FastAPIが生成するOpenAPIスキーマを基に @hey-api/openapi-ts でTypescriptの型を生成できます。これによりバックエンドとフロントエンドで型を安全に共有でき、実装の重複や齟齬を防ぎながら効率的に開発を進められます。

使ってみる

フロントエンドの作成

まずViteを使ってReactとTyepescriptで新規プロジェクトを作成します。
対話形式で、「Select a framework」と「Select a variant」を聞かれるのでそれぞれ「React」、「TypeScript」を選択します。

npm create vite@latest frontend

次に以下のコマンドで @hey-api/openapi-ts をインストールしてください。

npm install @hey-api/openapi-ts -D -E

インストールが完了したら、
frontend/package.jsonに以下を追加し、

frontend/package.json
"scripts": {
  "openapi-ts": "openapi-ts"
}

frontend/openapi-ts.config.tsに設定ファイルを作成し、以下の内容を記述します。

frontend/openapi-ts.config.ts
import { defineConfig } from '@hey-api/openapi-ts';

export default defineConfig({
  input: './openapi.json',  //エクスポートしたパス
  output: './api', //型定義を出力するパス
});

input は、OpenAPIスキーマのjsonファイルのパスを指定します。このあとFastAPIで出力されるファイルをここに配置します。

output は、出力先のフォルダを指定します。今回フロントエンド側で参照する型定義のファイルは{output}/types.gen.tsに記述されます。

これで、npm run openapi-tsのコマンドで @hey-api/openapi-ts を使用することができるようになりました。inputのファイルパスにOpenAPIのスキーマが記述されたjsonファイルを置く必要があるので、続いてバックエンド側も作成していきます。

バックエンドの作成

バックエンド側ではbackendフォルダを作成し、backend/app/main.pyに以下を配置しました。

ここで定義したUser, GetUserOutput, ResisterUserInput, ResisterUserOutput の型をTypescript側でも参照できるようにしたい、ということが今回のモチベーションです。

backend/app/main.py
from fastapi import FastAPI
from pydantic import BaseModel, Field

app = FastAPI(
    title="Mock Backend",
    description="Backend API",
    version="0.0.1",
    docs_url="/docs",
    openapi_url="/openapi.json",
)

class User(BaseModel):
    id: int = Field()
    name: str = Field()

class GetUserOutput(BaseModel):
    user: User
    message: str

class ResisterUserInput(BaseModel):
    user: User 

class ResisterUserOutput(BaseModel):
    message: str

@app.get("/user", response_model=GetUserOutput)
def get_user():

    return GetUserOutput(
        user=User(
            id=1,
            name="name",
        ),
        message="success"
    )

@app.post("/user", response_model=ResisterUserOutput)
def post_user(user: ResisterUserInput):
    print(user)

    return ResisterUserOutput(
        message="success"
    )

backend/requirements.txt
fastapi==0.115.0
pydantic==2.9.2

現状のフォルダ構成はこのような感じになります。(直接いじらない箇所は省略)

root/
├── backend/
│   ├──app/
│   │  ├── __init__.py
│   │  └── main.py
│   └── requirements.txt
└── frontend/
    ├── openapi-ts.config.ts
    ├── package.json
    ├ ...

@hey-api/openapi-tsの実行

続いてプロジェクトのルートで以下のコマンドを実行します。

# backendに移動
cd backend

# OpenAPIスキーマjsonファイルをfrontend/openapi-ts.config.tsのinputで指定したパスにエクスポート
python -c "import app.main; import json; print(json.dumps(app.main.app.openapi()))" > ../frontend/openapi.json

# frontendに移動
cd .. && cd frontend

# openapi-tsを実行
npm run openapi-ts

npm run openapi-tsを実行するとfrontend/openapi-ts.config.tsのoutputで指定したパスに結果が出力されます。多くのファイルが生成されますが、必要な型定義は、frontend/api/types.gen.tsに生成されます。

frontend/api/    ← frontend/openapi-ts.config.tsのoutputで指定したフォルダ
├── client.gen.ts
├── index.ts
├── sdk.gen.ts
├── types.gen.ts    ← ここに型が生成される
├── client/
│   ├── client.gen.ts
│   ├── index.ts
│   ├── types.gen.ts
│   └── utils.gen.ts
└── core/
    ├── auth.gen.ts
    ├── bodySerializer.gen.ts
    ├── params.gen.ts
    ├── pathSerializer.gen.ts
    ├── serverSentEvents.gen.ts
    ├── types.gen.ts
    └── utils.gen.ts

User, GetUserOutput, ResisterUserInput, ResisterUserOutputがバックエンドで定義した通りに生成されていることを確認できました。

frontend/api/types.gen.ts
// This file is auto-generated by @hey-api/openapi-ts

/**
 * GetUserOutput
 */
export type GetUserOutput = {
    user: User;
    /**
     * Message
     */
    message: string;
};

/**
 * ResisterUserInput
 */
export type ResisterUserInput = {
    user: User;
};

/**
 * ResisterUserOutput
 */
export type ResisterUserOutput = {
    /**
     * Message
     */
    message: string;
};

/**
 * User
 */
export type User = {
    /**
     * Id
     */
    id: number;
    /**
     * Name
     */
    name: string;
};

/**
 * HTTPValidationError
 */
export type HttpValidationError = {
    /**
     * Detail
     */
    detail?: Array<ValidationError>;
};

/**
 * ValidationError
 */
export type ValidationError = {
    /**
     * Location
     */
    loc: Array<string | number>;
    /**
     * Message
     */
    msg: string;
    /**
     * Error Type
     */
    type: string;
};

export type GetUserUserGetData = {
    body?: never;
    path?: never;
    query?: never;
    url: '/user';
};

export type GetUserUserGetResponses = {
    /**
     * Successful Response
     */
    200: GetUserOutput;
};

export type GetUserUserGetResponse = GetUserUserGetResponses[keyof GetUserUserGetResponses];

export type PostUserUserPostData = {
    body: ResisterUserInput;
    path?: never;
    query?: never;
    url: '/user';
};

export type PostUserUserPostErrors = {
    /**
     * Validation Error
     */
    422: HttpValidationError;
};

export type PostUserUserPostError = PostUserUserPostErrors[keyof PostUserUserPostErrors];

export type PostUserUserPostResponses = {
    /**
     * Successful Response
     */
    200: ResisterUserOutput;
};

export type PostUserUserPostResponse = PostUserUserPostResponses[keyof PostUserUserPostResponses];

export type ClientOptions = {
    baseUrl: `${string}://${string}` | (string & {});
};

FastAPIで生成されるOpenAPIスキーマは以下のようになっていました。

openapi.json
{
  "openapi": "3.1.0",
  "info": {
    "title": "Mock Backend",
    "description": "Backend API",
    "version": "0.0.1"
  },
  "paths": {
    "/user": {
      "get": {
        "summary": "Get User",
        "operationId": "get_user_user_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/GetUserOutput" }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Post User",
        "operationId": "post_user_user_post",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/ResisterUserInput" }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/ResisterUserOutput" }
              }
            }
          },
          "422": {
            "description": "Validation Error",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HTTPValidationError" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "GetUserOutput": {
        "properties": {
          "user": { "$ref": "#/components/schemas/User" },
          "message": { "type": "string", "title": "Message" }
        },
        "type": "object",
        "required": ["user", "message"],
        "title": "GetUserOutput"
      },
      "HTTPValidationError": {
        "properties": {
          "detail": {
            "items": { "$ref": "#/components/schemas/ValidationError" },
            "type": "array",
            "title": "Detail"
          }
        },
        "type": "object",
        "title": "HTTPValidationError"
      },
      "ResisterUserInput": {
        "properties": { "user": { "$ref": "#/components/schemas/User" } },
        "type": "object",
        "required": ["user"],
        "title": "ResisterUserInput"
      },
      "ResisterUserOutput": {
        "properties": { "message": { "type": "string", "title": "Message" } },
        "type": "object",
        "required": ["message"],
        "title": "ResisterUserOutput"
      },
      "User": {
        "properties": {
          "id": { "type": "integer", "title": "Id" },
          "name": { "type": "string", "title": "Name" }
        },
        "type": "object",
        "required": ["id", "name"],
        "title": "User"
      },
      "ValidationError": {
        "properties": {
          "loc": {
            "items": { "anyOf": [{ "type": "string" }, { "type": "integer" }] },
            "type": "array",
            "title": "Location"
          },
          "msg": { "type": "string", "title": "Message" },
          "type": { "type": "string", "title": "Error Type" }
        },
        "type": "object",
        "required": ["loc", "msg", "type"],
        "title": "ValidationError"
      }
    }
  }
}

まとめ

今回は FastAPI が自動生成する OpenAPI スキーマ を使って、フロントエンド側で利用可能な 型定義やクライアントコードを @hey-api/openapi-ts で生成する方法 を試してみました。

実際に使ってみると、

  • FastAPI 側の定義がそのままOpenAPIに落とし込まれる
  • @hey-api/openapi-tsがOpenAPIをもとにTypeScript の型を自動生成してくれる
  • その結果、バックエンドとフロントエンドで 同じ「型のソース」を共有できる状態 が作れる

という体験が得られました。

齟齬や実装ミスを防ぎながら、より安全かつ効率的に開発を進められるようになると思うので活用していきたいです。

この記事をシェアする

facebookのロゴhatenaのロゴtwitterのロゴ

© Classmethod, Inc. All rights reserved.