開発中は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でのディレクトリ構成・モデル設計・テストで解説しています。