Python

React×FastAPI連携の手順|CORS・Viteプロキシ・型生成とDocker Composeで動かす【2026年版】

React×FastAPI連携の手順|CORS・Viteプロキシ・型生成とDocker Composeで動かす【2026年版】

開発中はVite開発サーバーとFastAPIが別々のプロセスで動き、Reactのコードはブラウザで実行されます。連携で決めることは「ブラウザから見てAPIを同じオリジンに置くか、別オリジンに置くか」の1点です。開発中はViteのプロキシで同一オリジンに見せ、本番はリバースプロキシかFastAPI自身の配信で同一オリジンにそろえれば、CORSを設定する場面は別ドメイン配信のときだけになります。本記事はFastAPI 0.143・Vite 8・React 19.3(いずれも2026年10月時点の最新版)を前提に、APIの作成、Reactからの呼び出し、OpenAPIからのTypeScriptクライアント生成、Docker Composeでの一括起動、本番での配信までをコード付きで説明します。

まとめ

  • 開発はViteのプロキシ:vite.config.tsのserver.proxyで/apiをFastAPIへ転送すれば、ブラウザからは同一オリジンに見えてCORS設定が要らない。
  • CORSは別オリジン配信のときだけ:許可オリジンはhttp://localhost:5173のように完全一致で列挙する。allow_origins=["*"]とallow_credentials=Trueの組み合わせは、Starlette 1.7.0ではどのオリジンにも資格情報付きの応答の読み取りを許可してしまうので使わない。
  • 型はOpenAPIから生成する:FastAPIが出す/openapi.jsonからHey API(@hey-api/openapi-ts)でTypeScriptの型と呼び出し関数を生成すると、Pydanticモデルの変更がReact側の型エラーとして見える。
  • Composeは3つの罠を先に潰す:fastapi devは--host 0.0.0.0、node_modulesは匿名ボリュームで隔離、postgres:18は/var/lib/postgresqlへマウントする。
  • 本番は同一オリジン:Nginxで/をReactのビルド、/apiをFastAPIへ振り分けるか、小規模ならFastAPIがビルドを配信する。FastAPIで配信する場合、StaticFiles(html=True)だけでは/todos/1のような深いパスが404になる。

React×FastAPIをつなぐ3つの構成と選び方

ブラウザは、ページのオリジン(プロトコル・ホスト・ポートの組)と違うオリジンへのfetchの結果を、サーバーが許可しない限りJavaScriptに渡しません。React(Vite開発サーバーは5173番)とFastAPI(8000番)はポートが違うので、そのままでは別オリジンです。つなぎ方は次の3通りです。

構成 ブラウザから見たAPI CORS設定 使う場面
Viteのプロキシ 同一オリジン(:5173/api) 不要 開発
CORSMiddleware 別オリジン(:8000) 必要 フロントとAPIを別ドメインで配信
リバースプロキシ/FastAPI配信 同一オリジン(/api) 不要 本番

迷ったら「開発はプロキシ、本番は同一オリジン」を選びます。CORSが必要になるのは、ブラウザから見てAPIのオリジンがページと違う構成で、典型はReactを静的ホスティングやCDN、APIを別ドメインで配信する場合です。この構成ではCookieのSameSite属性とCORSの資格情報の扱いを両方設計する必要があり、手間が一段増えます。本記事のディレクトリ構成は次のとおりです。

myapp/
├─ backend/
│  ├─ app/main.py
│  ├─ requirements.txt
│  └─ Dockerfile
├─ frontend/        # Viteで作成
│  ├─ src/
│  ├─ package.json
│  └─ vite.config.ts
└─ compose.yaml

React向けFastAPIのAPI作成とCORS設定

FastAPI自体のインストールやasync defとdefの使い分けはFastAPIとは|最短で動かす手順とasync/defの使い分けで扱っているので、ここでは連携に必要な部分に絞ります。FastAPI 0.143はPython 3.10以上が必要です。

Pydanticモデルによる入出力の型定義

Reactとやり取りするJSONの形はPydanticモデルで宣言します。宣言した型はリクエストの検証とOpenAPIスキーマの両方に使われ、後述のTypeScript生成の元になります。パスは/apiで始めておくと、プロキシやNginxでAPIとフロントを振り分けやすくなります。

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

app = FastAPI()


class TodoIn(BaseModel):
    title: str = Field(min_length=1, max_length=100)


class Todo(TodoIn):
    id: int
    done: bool = False


todos: list[Todo] = []


@app.get("/api/todos", tags=["todos"])
def list_todos() -> list[Todo]:
    return todos


@app.post("/api/todos", tags=["todos"], status_code=201)
def create_todo(body: TodoIn) -> Todo:
    todo = Todo(id=len(todos) + 1, title=body.title)
    todos.append(todo)
    return todo

requirements.txtにはfastapi[standard]を書き、fastapi dev app/main.pyで起動します。[standard]を付けないとfastapiコマンド(fastapi-cli)とUvicornが入りません。起動ログに出る/docsを開くと、FastAPIのSwagger UI(/docs)からAPIを試せます。

CORSMiddlewareの許可オリジンと資格情報の設定

Reactを別オリジンから配信する場合は、FastAPI側で許可するオリジンを登録します。FastAPI公式のCORSの説明どおり、Cookieを送るならallow_credentials=Trueにし、オリジン・メソッド・ヘッダーを具体的に書きます。JavaScriptで付けるAuthorizationヘッダーは資格情報の設定とは別扱いで、allow_headersに明示して許可します。

# backend/app/main.py(app = FastAPI() の直後に追記)
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],
    allow_credentials=True,
    allow_methods=["GET", "POST"],
    allow_headers=["Content-Type", "Authorization"],
)

オリジンは文字列の完全一致で比べられます。http://localhost:5173だけを許可した状態でhttp://127.0.0.1:5173からアクセスしたり、許可リストに末尾スラッシュ付きのhttp://localhost:5173/を書いたりすると、プリフライト(OPTIONS)が400と「Disallowed CORS origin」で拒否されます。

注意したいのがallow_origins=["*"]です。公式ドキュメントは資格情報を使う場合にワイルドカードを指定できないと書いていますが、Starlette 1.7.0のCORSMiddlewareの実装は、["*"]とallow_credentials=Trueを同時に指定するとリクエストのOriginをそのままAccess-Control-Allow-Originに返し、Access-Control-Allow-Credentials: trueを付けます。つまりエラーにはならず、https://evil.exampleのような任意のオリジンに資格情報付きの応答の読み取りを許可した状態になります。ブラウザがCookieを付けるかどうかは別にSameSite属性で決まり、SameSite=Noneで発行したCookieなら、他サイトのページから利用者のCookie付きでAPIを呼んで応答を読めてしまいます。「とりあえず全部許可」で動いたからといって、この設定を本番に残してはいけません。

ViteによるReactの作成とFastAPIの呼び出し

フロントはViteで作ります。create-react-appはReact公式が2025年2月14日の「Sunsetting Create React App」で新規利用の非推奨を告知しており、新しく選ぶ理由はありません。React 19の変更点はReact 19とは?18からの変更点・新機能にまとめています。

ViteでのReactプロジェクト作成(Node.js 20.19以上)

npm create vite@latest frontend -- --template react-ts
cd frontend
npm install

Vite 8とcreate-vite 9はNode.js 20.19以上または22.12以上が必要です。create-vite 9のReactテンプレートは、リンターに標準でESLintではなくOxlintを入れます(ESLintにしたい場合は--eslintを付けます)。

Viteプロキシによる/apiの転送設定

Viteのserver.proxyに/apiを登録すると、ブラウザは5173番にだけ話しかけ、Vite開発サーバーが裏でFastAPIへ中継します。転送先を環境変数で差し替えられるようにしておくと、ローカル実行とDocker Composeで同じ設定ファイルを使えます。

// frontend/vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  server: {
    host: true,
    port: 5173,
    proxy: {
      // ローカルは localhost:8000、Composeでは API_ORIGIN=http://api:8000
      "/api": process.env.API_ORIGIN ?? "http://localhost:8000",
    },
  },
});

npm run devでVite開発サーバーを起動し、http://localhost:5173を開きます(FastAPIは別のターミナルでfastapi dev app/main.py)。プロキシ経由ならブラウザから見たAPIはhttp://localhost:5173/api/todosで同一オリジンなので、前章のCORSMiddlewareは開発中は不要です。

fetchによるGET・POSTと422エラーの表示

Reactからは相対パス/api/todosで呼びます。FastAPIは入力がPydanticモデルの制約を満たさないと422を返し、本文のdetailに配列でエラーを入れます。空文字を送った場合の応答は{"detail":[{"type":"string_too_short","loc":["body","title"],"msg":"String should have at least 1 character",...}]}です。

// frontend/src/App.tsx
import { useEffect, useState } from "react";

type Todo = { id: number; title: string; done: boolean };
type ValidationError = { detail: { loc: (string | number)[]; msg: string }[] };

export default function App() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [title, setTitle] = useState("");
  const [error, setError] = useState("");

  useEffect(() => {
    fetch("/api/todos")
      .then((res) => res.json())
      .then((data: Todo[]) => setTodos(data));
  }, []);

  async function add() {
    const res = await fetch("/api/todos", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ title }),
    });
    if (res.status === 422) {
      const body: ValidationError = await res.json();
      setError(body.detail.map((d) => `${d.loc.join(".")}: ${d.msg}`).join(" / "));
      return;
    }
    const todo: Todo = await res.json();
    setTodos((prev) => [...prev, todo]);
    setTitle("");
    setError("");
  }

  return (
    <main>
      <input value={title} onChange={(e) => setTitle(e.target.value)} />
      <button onClick={add}>追加</button>
      {error && <p>{error}</p>}
      <ul>
        {todos.map((t) => (
          <li key={t.id}>{t.title}</li>
        ))}
      </ul>
    </main>
  );
}

最小例なので、res.okによる500などの判定と、APIが落ちているときのtry〜catchは省いています。実運用ではこの2つを足します。このコードのもう1つの弱点は、Todo型をPython側と二重に手書きしている点です。FastAPIで項目を足しても、React側の型は古いまま型チェックを通ります。次章でこれを生成に置き換えます。

OpenAPIからのTypeScriptクライアント生成と型の同期

FastAPIはPydanticモデルから/openapi.jsonを自動で出力します。FastAPI公式のクライアント生成の解説はTypeScript向けにHey API(@hey-api/openapi-ts)を案内しており、FastAPI公式のfull-stack-fastapi-templateもReact+Vite+TanStack Query/Router+Tailwind CSS+shadcn/uiの構成でこの生成クライアントを使っています。手書きの型をやめてAPIの変更のたびにクライアントを再生成すれば、React側のコードと合わなくなった箇所がビルド時の型エラーとして出ます。

generate_unique_id_functionによる関数名の調整

何もしないと、生成される関数名はFastAPI既定のoperationId(例:create_todo_api_todos_post)由来の長い名前になります。タグと関数名からIDを作る関数を渡すと、todosCreateTodoのような読める名前になります。

# backend/app/main.py(app = FastAPI() を置き換え)
from fastapi.routing import APIRoute


def custom_generate_unique_id(route: APIRoute) -> str:
    return f"{route.tags[0]}-{route.name}"


app = FastAPI(generate_unique_id_function=custom_generate_unique_id)

タグの無いルートがあるとroute.tags[0]でIndexErrorになるので、APIルートには必ずタグを付けます。

Hey API 0.99の生成クライアントによるAPI呼び出し

FastAPIを起動した状態で、frontendで次を実行します。@hey-api/openapi-ts 0.99はNode.js 22.18以上が必要で、TypeScriptをpeer依存として要求します。TypeScriptが入っていないディレクトリでnpxだけ実行すると「Cannot read properties of undefined (reading ‘AnyKeyword’)」で止まります。Hey API公式は破壊的変更に備えて版を固定するよう案内しているので、-Eで版を固定します。

npm install -D -E @hey-api/[email protected]
npx openapi-ts -i http://localhost:8000/openapi.json -o src/client

src/clientにtypes.gen.ts(型)とsdk.gen.ts(呼び出し関数)などが生成されます。前章のApp.tsxは、手書きの型とfetchを生成物に置き換えるとこうなります。

// frontend/src/App.tsx(生成クライアント版・JSXは前章と同じ)
import { useEffect, useState } from "react";
import { todosCreateTodo, todosListTodos } from "./client";
import type { Todo } from "./client";

export default function App() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [title, setTitle] = useState("");
  const [error, setError] = useState("");

  useEffect(() => {
    todosListTodos().then(({ data }) => setTodos(data ?? []));
  }, []);

  async function add() {
    const { data, error } = await todosCreateTodo({ body: { title } });
    if (error) {
      setError(error.detail?.map((d) => d.msg).join(" / ") ?? "送信に失敗しました");
      return;
    }
    setTodos((prev) => [...prev, data]);
    setTitle("");
    setError("");
  }
  // return 以下は前章と同じ
}

生成された関数は{ data, error }を返し、errorには422のdetailの型まで付いています。bodyにtitle以外の項目を渡すと型エラーになるので、FastAPI側でモデルを変えたら生成し直してビルドすれば、React側で使っている項目のうち型が合わなくなった箇所が分かります。項目の追加のように互換性のある変更や、React側で使っていない項目の変更はエラーになりません。生成コマンドはpackage.jsonのscriptsに登録しておきます。

Docker ComposeでのReact・FastAPI・PostgreSQL一括起動

開発環境をComposeにまとめると、docker compose upの1コマンドでDB・API・フロントがそろいます。本番用のイメージ作成やNginx配信、クラウドへのデプロイはFastAPI×ReactのDocker本番デプロイで扱うので、ここでは開発用の構成に絞ります。

FastAPIのDockerfile

# backend/Dockerfile
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["fastapi", "run", "app/main.py", "--port", "8000"]

requirements.txtはfastapi[standard]と、PostgreSQLへ接続するならpsycopg[binary]です。CMDは本番向けのfastapi runにしておき、開発時はComposeのcommandでfastapi devに上書きします。

compose.yamlとハマりやすい3つの設定

# compose.yaml(開発用)
services:
  db:
    image: postgres:18
    environment:
      POSTGRES_PASSWORD: devpass
      POSTGRES_DB: appdb
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d appdb"]
      interval: 5s
      retries: 10
    volumes:
      - db-data:/var/lib/postgresql

  api:
    build: ./backend
    command: fastapi dev app/main.py --host 0.0.0.0 --port 8000
    environment:
      DATABASE_URL: postgresql+psycopg://postgres:devpass@db:5432/appdb
    ports:
      - "8000:8000"
    volumes:
      - ./backend/app:/app/app
    depends_on:
      db:
        condition: service_healthy

  web:
    image: node:24-slim
    working_dir: /app
    command: sh -c "npm ci && npm run dev -- --host 0.0.0.0"
    environment:
      API_ORIGIN: http://api:8000
    ports:
      - "5173:5173"
    volumes:
      - ./frontend:/app
      - /app/node_modules

volumes:
  db-data:

なお、本記事のAPIはTodoをPythonのリストに持つだけで、DATABASE_URLはまだ使っていません。DBコンテナを起動しただけでは保存されず、apiを再起動するとデータは消えます。この構成でdocker compose up --buildを実行すると、dbがhealthyになってからapiが起動し、http://localhost:5173/api/todosへのPOSTがViteのプロキシ経由でFastAPIに届きます。既存の記事やテンプレートを流用すると、次の3か所で止まりがちです。

  • fastapi devのバインド先:既定は127.0.0.1なので、--host 0.0.0.0を付けないとポートを公開してもコンテナの外から届きません。
  • node_modulesの取り違え:Vite 8のバンドラRolldownはOSごとのネイティブバイナリを使います。Macでnpm installしたnode_modulesをLinuxコンテナにそのままマウントすると「Cannot find native binding」で起動しません。- /app/node_modulesの匿名ボリュームでホスト側を隠し、コンテナ内でnpm ciします。
  • PostgreSQL 18のマウント先:公式postgresイメージは18からPGDATAが/var/lib/postgresql/18/dockerに変わりました。従来どおり/var/lib/postgresql/dataにボリュームを付けると、コンテナが「Error: in 18+, these Docker images are configured to store database data in a format which is compatible with “pg_ctlcluster”」で終了します。マウント先は/var/lib/postgresqlです。

apiのdepends_onにcondition: service_healthyを付けているのは、Docker公式の起動順の説明どおり、単なるdepends_onはコンテナの起動順しか保証せず、DBが接続を受け付けるまでは待たないためです。DBのモデル層はSQLModelかFastAPIとSQLAlchemyのモデル設計を参照してください。web側から見たlocalhostはwebコンテナ自身なので、プロキシ先はAPI_ORIGIN=http://api:8000のようにサービス名で指定します。

本番環境でのReactとFastAPIの同一オリジン配信

本番ではVite開発サーバーを動かさず、npm run buildで作ったdistをAPIと同じオリジンで配信します。トラフィックや静的ファイルのキャッシュ制御が要る構成ならNginx、社内ツールのように1コンテナで済ませたい構成ならFastAPIでの配信を選びます。要件定義から業務システムとして一括で依頼したい場合は、弊社のフルスクラッチ開発でご相談を受け付けています。

Nginxのリバースプロキシによる振り分け

Nginxが/でdistを返し、/api/をFastAPIコンテナへ転送します。SPAの深いパスはtry_files $uri /index.html;でindex.htmlに戻します。本番用のComposeファイル、マルチステージのDockerfile、proxy_passの末尾スラッシュで/apiが剥がれる問題はFastAPI×ReactのDocker本番デプロイの記事で説明しています。

FastAPIによるReactビルドの配信とSPAフォールバック

app.mount("/", StaticFiles(directory="dist", html=True))で/と/assets/...は返せますが、React Routerの/todos/1のようなパスは対応するファイルが無いので404になります。html=Trueはディレクトリへのアクセスでindex.htmlを返す設定で、SPA用のフォールバックではありません。404のときだけindex.htmlを返すサブクラスを作ります。配信前にfrontendでnpm run buildを実行し、できたfrontend/distをbackend/distへコピーしておきます。下のコードはmain.pyの位置からbackend/distを指すので、起動する作業ディレクトリに左右されません。

# backend/app/main.py(APIルートの定義より後に追記)
from pathlib import Path

from fastapi.staticfiles import StaticFiles
from starlette.exceptions import HTTPException


class SPAStaticFiles(StaticFiles):
    async def get_response(self, path: str, scope):
        try:
            return await super().get_response(path, scope)
        except HTTPException as exc:
            if exc.status_code != 404 or path == "api" or path.startswith(("api/", "assets/")):
                raise
            return await super().get_response("index.html", scope)


DIST = Path(__file__).resolve().parent.parent / "dist"  # backend/dist
app.mount("/", SPAStaticFiles(directory=DIST, html=True), name="spa")

mountはAPIルートの後に書きます。先に書くとすべてのパスを静的ファイル側が受け取ります。apiとassets/を除外する条件は、存在しないAPIのパスや欠けたJSファイルにindex.htmlが200で返るのを防ぐためのものです。これで/api・/api/nothing・/assets/missing.jsは従来どおりJSONの404になり、/todos/1だけがindex.htmlを返します。

React×FastAPI連携でよく出るエラーと原因

連携のエラーの多くは、オリジンやホスト名の取り違えです。

症状 原因 対処
ブラウザに「blocked by CORS policy」 別オリジンからの呼び出し Viteのプロキシにするか許可オリジンを登録
OPTIONSが400「Disallowed CORS origin」 許可オリジンと文字列が不一致 localhostと127.0.0.1、末尾スラッシュを確認
Viteのログに「http proxy error」ECONNREFUSED API未起動、またはホスト名・ポートの誤り API起動を確認し、コンテナ内はサービス名で指定
APIに外から接続できない fastapi devが127.0.0.1で待受 –host 0.0.0.0を付ける
「Cannot find native binding」 ホストのnode_modulesを流用 匿名ボリュームとコンテナ内のnpm ci
POSTが422 送ったJSONがモデルの制約外 detailのlocとmsgを確認
リロードすると404 SPAのフォールバック未設定 try_filesかSPAStaticFiles

CORSのエラーはブラウザの開発者ツールのコンソールに出ますが、原因の多くはサーバー側の応答ヘッダーにあります。ネットワークタブでOPTIONSリクエストの応答にAccess-Control-Allow-Originが付いているかを最初に見ます。

よくある質問

ReactとFastAPIは同じポートで動かせますか?

プロセスは別ポート(Viteが5173、FastAPIが8000)ですが、開発はViteのプロキシ、本番はNginxかFastAPIの配信で、利用者からは1つのポートに見せられます。

ReactとPythonでWebアプリを作るならFastAPIとDjangoのどちらがよいですか?

画面をすべてReactで作り、PythonはJSON APIだけを返すならFastAPIが向いています。Pydanticの型からOpenAPIが自動で出るため、本記事のようにTypeScriptの型まで生成できます。管理画面やテンプレート描画、認証の仕組みをフレームワークに任せたいならDjangoです。

create-react-appは今も使えますか?

新規プロジェクトでは使いません。React公式は2025年2月14日に新規利用の非推奨を告知し、Next.jsやReact Routerなどのフレームワーク、またはViteなどのビルドツールへの移行を案内しています。

React+FastAPIのテンプレートはありますか?

FastAPI公式のfull-stack-fastapi-templateがあります。React・TypeScript・Vite・生成クライアント・SQLModel・PostgreSQL・Docker Compose・Traefikまで入った構成です。JWT認証やメールによるパスワード再設定まで最初から要るならテンプレートから始め、構成を自分で理解したいなら本記事のように小さく組むのが向いています。

FastAPIのディレクトリ構成やベストプラクティスはどこを見ればよいですか?

APIが大きくなったら、ルーターをファイルごとに分け、DBモデルとPydanticのスキーマを分けるのが基本です。SQLAlchemyを使った具体的な構成はFastAPIのベストプラクティス|SQLAlchemyでのディレクトリ構成・モデル設計・テストで解説しています。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.07.21 テックブログ Apache Tomcatの脆弱性一覧【2026年10月】最新版11.0.27・10.1.61・9.0.123とCVE-2026-59083ほかの影響確認・対応手順
  2. 2026.10.22 テックブログ Tomcatバージョン一覧とサポート期限【2026年10月】9.0系は2027年3月末で終了・9.1系と11.0系への移行判断
  3. 2026.10.21 テックブログ NEC委託先の不正アクセスとは|顧客システム暗号化・情報漏えいの経緯と委託元が点検する正規アカウント
  4. 2026.05.15 テックブログ scrcpyの最新版は5.0|ハードウェアデコード対応とインストール・使い方【2026年10月】
  5. 2026.10.21 テックブログ ふるさとチョイス不正アクセスで445万件流出:対象確認・パスワード変更と再開時期

RELATED POSTS 関連記事

目次