Skip to content

フロントエンド配信(app.frontend)

FastAPI 0.138.0 で app.frontend() が追加された。ビルド済みのフロントエンド(HTML / CSS / JS)を 1 行で配信するためのメソッド。StaticFiles と違って追加の import が要らないし、 API のルートと同じアプリ・同じポートに同居させても API が隠れない。

cf. Frontend - FastAPI


ファイル構成

main.py と、フロントエンド用の dist/ を用意する。 dist/ の中に HTML と JS を置くだけにして試してみる。

.
├── main.py
└── dist
    ├── index.html
    └── app.js

main.py

main.py
from fastapi import FastAPI

app = FastAPI()

app.frontend("/", directory="dist")


@app.get("/inc")
def inc(x: int):
    return {"result": x + 1}

app.frontend(配信パス, directory=配信するディレクトリ) で、 dist/ の中身を / 以下で配信する。

FastAPI は path operation(@app.get などで定義したルート)を先にチェックして、どれにもマッチしなかったリクエストだけを dist/ にフォールバックさせる。だから /inc/docs は今まで通り API・Swagger UI として動くし、それ以外のパスがフロントエンドになる。

dist/ は先に作っておく

app.frontend() は起動時に directory の存在を確認する(check_dir=True がデフォルト)。 dist/ が無いと起動に失敗するので、先に下の HTML と JS を作っておく。存在チェックを後回しにしたいときは check_dir=False を渡す。


dist/index.html

数字を入れて +1 ボタンを押すと結果を表示するだけのページ。 /app.js を読み込む。

dist/index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>+1 calculator</title>
  </head>
  <body>
    <h1>+1 calculator</h1>
    <input id="x" type="number" value="0">
    <button id="run">+1</button>
    <p id="result"></p>
    <script src="/app.js"></script>
  </body>
</html>

dist/app.js

ボタンが押されたら /inc?x=... を叩いて、返ってきた result を画面に出す。

dist/app.js
const input = document.getElementById("x");
const button = document.getElementById("run");
const result = document.getElementById("result");

button.addEventListener("click", async () => {
  const response = await fetch(`/inc?x=${input.value}`);
  const data = await response.json();
  result.textContent = `result: ${data.result}`;
});

フロントエンドと API が同じオリジン(同じホスト・ポート)なので、 CORS の設定は要らない。


動かす

uv run fastapi dev

ブラウザで http://localhost:8000/ を開くと index.html が表示される。数字を入れて +1 を押すと、 /inc のレスポンスがそのまま画面に出る。

+1 calculator のデモ画面。33 を入力して +1 を押すと result: 34 が表示される

それぞれのパスがどこに向くかは下のとおり。

パス 中身
/ dist/index.html
/app.js dist/app.js
/inc?x=1 API({"result": 2})。path operation が優先されるので dist/ は見に行かない

StaticFiles との違い

似たことは StaticFiles でもできるけど、いくつか手間が違う。

  • import: StaticFilesfrom fastapi.staticfiles import StaticFiles が要る。 app.frontend()FastAPI だけで使える。
  • 優先順位: StaticFiles/ にマウントすると、その配下のパスはマウントが受け持つので、 API ルートと同居させるときに順番を気にする必要がある。 app.frontend() は path operation を先にチェックすると決まっているので、 API の邪魔をしない。
  • SPA 対応: app.frontend() は存在しないパスを index.html404.html に流す fallback を持っている。

SPA のフォールバック

React や Vue などでクライアントサイドルーティングを使うときは fallback を指定する。

  • fallback="auto"(デフォルト) … dist/404.html があればそれを、無ければ index.html をフォールバックに使う。
  • fallback="index.html" … 存在しないパスへのブラウザのナビゲーションを index.html に流して、フロント側のルーターに処理させる。 JS や CSS、画像など実ファイルが無い場合は 404 のまま。
  • fallback="404.html"404.html404 ステータスで返す。
  • fallback=None … フォールバックしない(通常の 404)。

今回のような単一の index.html のデモはデフォルト(auto)のままで動く。例えば存在しない /not-exist をブラウザで開くと、 URL は /not-exist のまま index.html が返る。一方、 /favicon.ico のような存在しないアセットは 404 のまま。

FastAPI のログ

INFO   127.0.0.1:50907 - "GET /not-exist HTTP/1.1" 200
INFO   127.0.0.1:50907 - "GET /app.js HTTP/1.1" 200
INFO   127.0.0.1:50907 - "GET /favicon.ico HTTP/1.1" 404