本文 AI 產出,尚未審核

FastAPI 課程 – 靜態檔案與模板(Static & Templates)

使用模板實作登入頁與表單頁


簡介

在 Web 開發中,前端畫面後端 API 必須緊密合作,才能提供使用者完整的互動體驗。FastAPI 雖然以「高效能 API 框架」聞名,但它同樣支援 Jinja2 等模板引擎,讓我們可以直接在同一個專案裡呈現 HTML 頁面、處理表單提交、甚至管理靜態資源(CSS、JS、圖片)。

對於 登入頁註冊表單資料輸入頁 等常見需求,使用模板的好處包括:

  1. 分離關注點 – HTML 與 Python 程式碼分開管理,維護成本降低。
  2. 即時渲染 – 依據使用者狀態(如錯誤訊息、預設值)動態產生頁面。
  3. 快速開發 – 結合 FastAPI 的路由與依賴注入機制,表單驗證與回應可以在同一層級完成。

本篇文章將從 設定靜態檔案與模板目錄建立登入表單處理表單送出,一步步示範如何在 FastAPI 中完成完整的登入流程,並分享常見陷阱與最佳實踐,讓你能在實務專案中快速上手。


核心概念

1. 設定靜態檔案與模板目錄

FastAPI 內建 StaticFilesJinja2Templates 兩個工具,分別負責提供靜態資源(如 CSS、JS)與渲染 HTML 模板。

# main.py
from fastapi import FastAPI, Request, Form, status
from fastapi.responses import RedirectResponse, HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

app = FastAPI()

# 1️⃣ 讓 /static 路徑對應到 ./static 目錄
app.mount("/static", StaticFiles(directory="static"), name="static")

# 2️⃣ 設定模板目錄為 ./templates
templates = Jinja2Templates(directory="templates")

重點app.mount 必須在所有路由之前呼叫,否則靜態檔案會被視為普通路由而產生 404。


2. 建立基本的登入表單模板

以下是一個簡單的 login.html,使用 Bootstrap 4 提供基礎樣式。模板放在 templates/login.html

<!-- templates/login.html -->
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>登入 - FastAPI Demo</title>
    <link rel="stylesheet" href="{{ url_for('static', path='css/bootstrap.min.css') }}">
</head>
<body class="bg-light">
<div class="container mt-5">
    <h2 class="mb-4">登入系統</h2>

    {% if error_msg %}
    <div class="alert alert-danger">{{ error_msg }}</div>
    {% endif %}

    <form method="post" action="{{ url_for('login') }}">
        <div class="form-group">
            <label for="username">使用者名稱</label>
            <input type="text" class="form-control" id="username" name="username"
                   value="{{ form_data.username | default('') }}" required>
        </div>
        <div class="form-group">
            <label for="password">密碼</label>
            <input type="password" class="form-control" id="password" name="password" required>
        </div>
        <button type="submit" class="btn btn-primary">登入</button>
    </form>
</div>
</body>
</html>
  • {{ url_for('static', path='css/bootstrap.min.css') }}:透過 url_for 產生靜態檔案的完整 URL,確保在不同部署環境下路徑正確。
  • {{ error_msg }}{{ form_data.username }}:由後端傳入的變數,用於顯示錯誤訊息或回填使用者先前輸入的資料。

3. 顯示登入頁的 GET 路由

# main.py(續)
@app.get("/login", response_class=HTMLResponse, name="login")
async def login_page(request: Request):
    """
    渲染登入頁面,初始不帶任何錯誤訊息或表單資料。
    """
    return templates.TemplateResponse(
        "login.html",
        {"request": request, "error_msg": None, "form_data": {}}
    )
  • request 必須傳入模板上下文,否則 url_for 會找不到 request 物件。
  • name="login" 為路由命名,方便在模板中使用 url_for('login') 產生表單 action

4. 處理表單提交的 POST 路由

# main.py(續)
@app.post("/login", response_class=HTMLResponse)
async def login_submit(
    request: Request,
    username: str = Form(...),
    password: str = Form(...)
):
    """
    接收表單資料,驗證使用者身分,成功則導向首頁,失敗回傳錯誤訊息。
    """
    # 這裡僅示範簡單的硬編碼驗證,實務上請使用資料庫 + 哈希密碼
    if username == "admin" and password == "secret":
        response = RedirectResponse(url="/", status_code=status.HTTP_302_FOUND)
        # 設定簡易的 session cookie(示範用,實務上建議使用安全的 JWT / OAuth)
        response.set_cookie(key="user", value=username, httponly=True)
        return response

    # 驗證失敗 → 重新渲染登入頁,帶入錯誤訊息與已輸入的使用者名稱
    return templates.TemplateResponse(
        "login.html",
        {
            "request": request,
            "error_msg": "帳號或密碼錯誤,請再試一次。",
            "form_data": {"username": username}
        }
    )
  • Form(...) 讓 FastAPI 自動把 application/x-www-form-urlencodedmultipart/form-data 轉成參數。
  • 重要:若驗證失敗,不要直接回傳 401,而是重新渲染表單並顯示錯誤,這樣使用者體驗較好。
  • 這裡示範了 RedirectResponse,在成功登入後導向首頁,同時寫入一個簡易的 cookie 作為登入狀態。

5. 建立受保護的首頁範例

# main.py(續)
@app.get("/", response_class=HTMLResponse)
async def home(request: Request, user: str = None):
    """
    只要有 `user` cookie 即視為已登入,否則導回登入頁。
    """
    if not request.cookies.get("user"):
        return RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)

    return templates.TemplateResponse(
        "home.html",
        {"request": request, "user": request.cookies.get("user")}
    )

home.html(放在 templates/home.html):

<!-- templates/home.html -->
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>歡迎 {{ user }}</title>
    <link rel="stylesheet" href="{{ url_for('static', path='css/bootstrap.min.css') }}">
</head>
<body class="bg-light">
<div class="container mt-5">
    <h2 class="mb-4">Hello, {{ user }}!</h2>
    <p>這是一個使用 FastAPI + Jinja2 渲染的簡易首頁。</p>
    <a href="/logout" class="btn btn-secondary">登出</a>
</div>
</body>
</html>

6. 登出路由(清除 Cookie)

@app.get("/logout")
async def logout():
    """
    清除登入 cookie,並導回登入頁。
    """
    response = RedirectResponse(url="/login", status_code=status.HTTP_302_FOUND)
    response.delete_cookie(key="user")
    return response

常見陷阱與最佳實踐

常見問題 可能原因 解決方式
模板找不到 url_for('static', …) 沒有在 TemplateResponse 中傳入 request 確保 {"request": request, …} 皆傳入模板
POST 表單送出 405 Method Not Allowed 路由只宣告了 GET,未提供 POST 為同一路徑同時寫 @app.get@app.post,或使用 @app.api_route(..., methods=["GET","POST"])
表單驗證失敗後錯誤訊息不顯示 模板未正確使用 error_msg 變數 login.html 加入 {% if error_msg %} 區塊
Cookie 不會被瀏覽器送出 設定了 domainpathsecure 不符合測試環境 測試時使用 httponly=Truesecure=False(正式環境請改為 secure=True 並使用 HTTPS)
靜態檔案載入慢或 404 app.mount 的路徑或 StaticFiles 目錄錯誤 確認 static 目錄結構與 app.mount("/static", StaticFiles(directory="static")) 一致

最佳實踐

  1. 分層目錄
    project/
    ├─ app/
    │   └─ main.py
    ├─ templates/
    │   ├─ login.html
    │   └─ home.html
    └─ static/
        ├─ css/
        │   └─ bootstrap.min.css
        └─ js/
    
  2. 使用 Pydantic 驗證表單(可用 Form + BaseModel)提升可讀性與可維護性。
  3. 安全性
    • 永遠使用 HTTPS
    • 密碼請儲存 bcrypt / argon2 雜湊值,切勿明碼儲存。
    • 采用 JWTOAuth2 替代簡易 cookie,避免 CSRF。
  4. 錯誤訊息國際化:將文字抽離成 i18n 檔案,未來支援多語系更方便。
  5. 測試:使用 TestClient 撰寫單元測試,確保 GET/POST 路由、重導向與 Cookie 行為正確。
# tests/test_auth.py
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_login_success():
    response = client.post("/login", data={"username": "admin", "password": "secret"})
    assert response.status_code == 302
    assert response.headers["location"] == "/"
    # 檢查 cookie 是否被設定
    assert "user=admin" in response.headers["set-cookie"]

實際應用場景

場景 為何使用模板 + 靜態檔案 相關程式碼片段
企業內部管理系統 需要快速打造登入、權限控制與統一 UI,且系統不一定要 SPA(單頁應用)。 login.html + home.html + Cookie 驗證
教學平台 每個課程都有獨立的說明頁與練習表單,使用模板可以共用版面,提升維護效率。 templates/course_{id}.html 動態渲染
原型開發 想在短時間內驗證概念,使用 FastAPI + Jinja2 能即時看到 UI 變化,無需額外前端框架。 static/css/style.css 直接寫在 templates
混合式 API + 前端 部分路由回傳 JSON,部分路由回傳 HTML,FastAPI 原生支援兩者共存。 @app.get("/api/users") 返回 JSON,@app.get("/login") 返回 HTML

總結

  • FastAPI 不僅是高效能的 API 框架,同時也能透過 Jinja2StaticFiles 直接提供完整的 Web 前端功能。
  • 透過本文的步驟,我們完成了 靜態資源掛載模板渲染表單送出與驗證、以及 簡易的登入/登出流程
  • 注意 request 物件 必須傳入模板、正確設定路由方法與靜態路徑,才能避免常見的 404/405 錯誤。
  • 為了 安全可維護性,建議在正式專案中使用 HTTPS、密碼雜湊、JWT,並將 UI 元件與驗證邏輯分離。

掌握了「使用模板作登入頁 / 表單頁」的核心技巧後,你可以快速擴展至 註冊、忘記密碼、個人設定 等更多功能,讓 FastAPI 成為 全端開發 的利器。祝開發順利,玩得開心! 🚀