🌐

OpenAPI から TypeScript の型を生成して、型安全に API を呼ぶ

に公開

概要

FastAPI は /openapi.json を自動で公開してくれるので、このスキーマを使うと
フロントエンド側の TypeScript 型を自動生成できます。

この記事では、以下の流れを最小構成で試します。

  • FastAPI で API を用意する
  • openapi-typescript で型定義(d.ts)を生成する
  • openapi-fetch で生成した型を使って API を呼び出す

主要な OpenAPI → TypeScript 生成ライブラリを比較した結果、
npm のダウンロード数が多かった openapi-typescript + openapi-fetch を採用しました。

fetch に文字列でエンドポイントを書くだけの実装から、
型安全にリクエストできる実装へ置き換えるところまでを扱います。

環境

動作確認は以下の環境で行いました。

  • OS: Windows 11 + WSL2 (Ubuntu 22.04)
  • フロントエンド
    • Node.js : 22.17.0
    • Next.js : 16.1.6
    • TypeScript : 5.9.3
    • openapi-typescript : 7.10.1
    • openapi-fetch : 0.15.0
  • バックエンド
    • Python : 3.13.2
    • FastAPI : 0.128.4

前準備

以下の前準備を行います。

  • バックエンド: FastAPI の最小 API(POST /hello)を用意して、OpenAPI スキーマ(/openapi.json)が出力される状態を作ります。
  • フロントエンド: Next.js の最小画面を用意し、最初は通常の fetch で API を呼び出せる状態を作ります。

バックエンド

uv でプロジェクトを作成し、ライブラリ(fastapi)をインストールします。

uv init backend
uv add fastapi[standard]

init で作成された main.py を以下の内容に変更し、/hello に POST できる状態にします。

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


class HelloRequest(BaseModel):
    name: str


@app.post("/hello")
def hello(req: HelloRequest):
    return {"message": f"hello {req.name}"}

fastapi を実行します。

uv run fastapi dev main.py

フロントエンド

Next.js のプロジェクトを作成します。
名前は frontend として、後はデフォルトで進めました。

npx create-next-app@latest
 What is your project named? frontend

page.tsx に入力欄・送信ボタン・結果表示だけ実装します。
この時点では OpenAPI スキーマは使っていません。

"use client";

import { useState } from "react";

export default function Home() {
  const [name, setName] = useState("");
  const [result, setResult] = useState<string | null>(null);

  const handleSubmit = async () => {
    setResult(null);
    const res = await fetch("http://localhost:8000/hello", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name }),
    });
    const data = await res.json();
    if (!res.ok) {
      setResult("エラー: " + JSON.stringify(data, null, 2));
      return;
    }
    setResult(JSON.stringify(data, null, 2));
  };

  return (
    <div style={{ padding: 40 }}>
      <h1>Hello API テスト</h1>
      <div style={{ marginTop: 20 }}>
        <input
          type="text"
          placeholder="name を入力"
          value={name}
          onChange={(e) => setName(e.target.value)}
          style={{ border: "1px solid #ccc", padding: 8, marginRight: 8 }}
        />
        <button onClick={handleSubmit} style={{ padding: "8px 16px" }}>
          送信
        </button>
      </div>
      {result !== null && (
        <pre style={{ marginTop: 20, background: "#f5f5f5", padding: 16 }}>
          {result}
        </pre>
      )}
    </div>
  );
}

OpenAPI スキーマを取得

FastAPI では /openapi.json を GET することで JSON 形式で取得できます。

curl http://localhost:8000/openapi.json > openapi.json

OpenAPI スキーマから TypeScript の型を生成する

以下のライブラリをインストールします。

  • openapi-typescript
  • openapi-fetch
npm install -D openapi-typescript
npm install openapi-fetch

以下のコマンドで OpenAPI のスキーマ から TypeScript の型を生成します。
-o に指定したファイルに生成されます

npx openapi-typescript openapi.json -o ./app/lib/api/schema.d.ts

page.tsx を修正する

主な修正点は以下です。

  • openapi-fetch と、生成した型 paths を import する
  • createClient<paths>() で API クライアントを作成する
  • 生の fetchclient.POST("/hello", { body: { name } }) に置き換える
  • レスポンスを data / error で分岐して表示する
"use client";

import { useState } from "react";
+import createClient from "openapi-fetch";
import type { paths } from "@/app/lib/api/schema";

+const client = createClient<paths>({ baseUrl: "http://localhost:8000" });

export default function Home() {
  const [name, setName] = useState("");
  const [result, setResult] = useState<string | null>(null);

  const handleSubmit = async () => {
    setResult(null);
-    const res = await fetch("http://localhost:8000/hello", {
-      method: "POST",
-      headers: { "Content-Type": "application/json" },
-      body: JSON.stringify({ name }),
-    });
-    const data = await res.json();
-    if (!res.ok) {
-      setResult("エラー: " + JSON.stringify(data, null, 2));
-      return;
-    }
-    setResult(JSON.stringify(data, null, 2));
+    const { data, error } = await client.POST("/hello", {
+      body: { name },
+    });
+   if (error) {
+     setResult("エラー: " + JSON.stringify(error, null, 2));
+   } else {
+     setResult(JSON.stringify(data, null, 2));
    }
  };

  return (
    <div style={{ padding: 40 }}>
      <h1>Hello API テスト</h1>
      <div style={{ marginTop: 20 }}>
        <input
          type="text"
          placeholder="name を入力"
          value={name}
          onChange={(e) => setName(e.target.value)}
          style={{ border: "1px solid #ccc", padding: 8, marginRight: 8 }}
        />
        <button onClick={handleSubmit} style={{ padding: "8px 16px" }}>
          送信
        </button>
      </div>
      {result !== null && (
        <pre style={{ marginTop: 20, background: "#f5f5f5", padding: 16 }}>
          {result}
        </pre>
      )}
    </div>
  );
}

まとめ

FastAPI の OpenAPI をそのまま型生成に使うことで、
フロントエンドの API 呼び出しをかなり安全にできます。

特に、openapi-fetch と組み合わせると、

  • パスやリクエストボディの型が効く
  • 実装と API 仕様のズレに気づきやすい
  • 手書きの型定義メンテナンスが減る

というメリットがありました。

運用では、バックエンド変更時に openapi.json と型生成を更新するコマンドを
package.json のスクリプトや CI に入れておくと、さらに扱いやすくなります。

一例として、以下のような感じになると思います。

{
  "scripts": {
    "openapi:fetch": "curl -fsS http://localhost:8000/openapi.json -o ./openapi.json",
    "openapi:typegen": "openapi-typescript ./openapi.json -o ./app/lib/api/schema.d.ts"
  }
}

Discussion