FastAPI 課程 – 請求與回應(Request & Response)
主題:使用 JSONResponse、HTMLResponse、PlainTextResponse
簡介
在 Web API 開發中,回應(Response) 的格式直接影響前端或其他服務端的使用體驗。FastAPI 預設會根據路由函式的回傳型別自動產生 JSON,然而實際專案往往需要傳回 HTML 頁面、純文字 或是 自訂的 JSON 結構(例如加入額外的 HTTP 標頭)。
本單元將聚焦於 FastAPI 內建的三種常用回應類別:JSONResponse、HTMLResponse 與 PlainTextResponse。透過這些工具,你可以:
- 精確控制回應的 Content‑Type
- 自訂 HTTP 狀態碼與標頭
- 在同一個應用中同時支援 API 與網頁渲染
掌握這些技巧,能讓你的服務更彈性、易於維護,同時提升使用者的使用體驗。
核心概念
1. 為什麼不直接回傳字典或字串?
FastAPI 允許直接回傳 dict、list、str 等 Python 原生物件,框架會自動轉換成 JSON 或純文字。但這樣的自動化有以下限制:
- 無法自行設定 HTTP 標頭(例如
Cache-Control、X-Custom-Header)。 - 回傳的 MIME type 只能是預設值(JSON 為
application/json、字串為text/plain)。 - 無法一次回傳多種資料類型(例如同時回傳 JSON 與檔案)。
使用 JSONResponse、HTMLResponse、PlainTextResponse,可以突破上述限制,讓回應更具可控性。
2. JSONResponse
JSONResponse 繼承自 Starlette 的 Response,專門用來回傳 JSON 資料。它的特點包括:
- 自訂
status_code:可以回傳 200、201、400、404 等任意狀態碼。 - 可加入自訂標頭:例如
X-Request-ID、Cache-Control。 - 支援
json_dumps參數,讓你自訂序列化方式(例如使用orjson)。
範例 1:基本的 JSONResponse
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
@app.get("/api/user/{user_id}")
async def get_user(user_id: int):
# 假設從資料庫取得使用者資料
user_data = {"id": user_id, "name": "Alice", "role": "admin"}
# 直接回傳 JSONResponse,狀態碼為 200
return JSONResponse(content=user_data)
說明
content參數接受任何可 JSON 序列化的 Python 物件。- 若未指定
status_code,預設為 200。
範例 2:自訂標頭與狀態碼
@app.post("/api/items")
async def create_item(item: dict):
# 假設成功寫入資料庫
response_data = {"message": "Item created", "item": item}
headers = {"X-Request-ID": "12345abcde", "Cache-Control": "no-store"}
return JSONResponse(
content=response_data,
status_code=201,
headers=headers
)
重點:
headers必須是dict[str, str],FastAPI 會自動合併到回應中。
範例 3:使用 orjson 提升序列化效能
import orjson
from fastapi.responses import JSONResponse
class ORJSONResponse(JSONResponse):
"""自訂 JSONResponse,使用 orjson 作為序列化工具"""
def render(self, content: any) -> bytes:
return orjson.dumps(content)
@app.get("/fast")
async def fast_endpoint():
data = {"msg": "使用 orjson 序列化更快"}
return ORJSONResponse(content=data)
說明
- 只要覆寫
render方法,即可替換底層的 JSON 序列化函式。orjson在大資料量時效能顯著提升。
3. HTMLResponse
當你的 FastAPI 同時扮演 API 與 Web Server 時,HTMLResponse 能直接回傳 HTML 內容,避免額外使用模板引擎(如 Jinja2)時的繁瑣設定。
範例 4:回傳簡易 HTML 頁面
from fastapi.responses import HTMLResponse
@app.get("/welcome", response_class=HTMLResponse)
async def welcome_page():
html_content = """
<html>
<head><title>歡迎使用 FastAPI</title></head>
<body>
<h1>🚀 歡迎來到 FastAPI 世界!</h1>
<p>這是一個簡易的 HTML 回應示範。</p>
</body>
</html>
"""
return HTMLResponse(content=html_content, status_code=200)
技巧:在路由裝飾器裡使用
response_class=HTMLResponse,可以讓 IDE 更清楚回傳類型。
範例 5:結合 Jinja2 與 HTMLResponse
from fastapi import Request
from fastapi.templating import Jinja2Templates
from fastapi.responses import HTMLResponse
templates = Jinja2Templates(directory="templates")
@app.get("/profile/{username}", response_class=HTMLResponse)
async def profile(request: Request, username: str):
# 假設從資料庫取得使用者資訊
user = {"username": username, "age": 28, "city": "Taipei"}
# 使用 Jinja2 渲染模板,最後交給 HTMLResponse
return templates.TemplateResponse("profile.html", {"request": request, "user": user})
說明
TemplateResponse本身已經是HTMLResponse的子類別,直接回傳即可。- 若需要額外設定標頭,只要在
TemplateResponse後再包一層HTMLResponse。
4. PlainTextResponse
雖然字串回傳會自動轉為 text/plain,但使用 PlainTextResponse 可以 明確宣告 回應類型,並且方便加入自訂標頭或狀態碼。
範例 6:回傳純文字訊息
from fastapi.responses import PlainTextResponse
@app.get("/ping")
async def ping():
# 常見的健康檢查端點
return PlainTextResponse(content="pong", status_code=200)
範例 7:回傳錯誤訊息與自訂標頭
@app.get("/error")
async def error_demo():
error_msg = "發生未預期的錯誤,請稍後再試"
headers = {"Retry-After": "30"} # 建議客戶端 30 秒後重試
return PlainTextResponse(content=error_msg, status_code=503, headers=headers)
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
忘記設定 media_type |
若回傳的內容與 Content-Type 不符,瀏覽器或客戶端可能無法正確解析。 |
使用 JSONResponse(media_type="application/vnd.api+json") 或自行指定 media_type。 |
直接回傳 dict 但需要自訂標頭 |
直接回傳 dict 時無法加入自訂標頭。 |
改用 JSONResponse,把標頭放在 headers 參數。 |
| HTMLResponse 中的字元編碼問題 | 若 HTML 內容包含非 ASCII 字元,未指定編碼會導致亂碼。 | 確保在 <meta charset="UTF-8"> 中宣告,或在 HTMLResponse 加上 charset="utf-8"。 |
| 大量資料直接用 JSONResponse | 大量資料序列化會佔用大量記憶體,導致效能瓶頸。 | 使用 StreamingResponse 搭配生成器,或採用 orjson、ujson。 |
忘記在路由裝飾器中宣告 response_class |
IDE 可能無法正確推斷回傳類型,導致自動完成失效。 | 明確寫 @app.get(..., response_class=HTMLResponse)。 |
最佳實踐:
- 統一回應結構:即使使用
JSONResponse,也建議包一層標準格式(如{ "code": 0, "data": ..., "msg": "成功" }),方便前端統一處理。 - 使用
status_code代表業務狀態:例如 201 代表資源建立成功,404 代表找不到資源,而非僅用 200 搭配訊息。 - 適度自訂
media_type:對於特定 API(如 GraphQL)或自訂協議,記得改變media_type。 - 將共用標頭抽離成 Middleware:如果每個回應都需要
X-Request-ID,可以寫一個 Middleware 自動加入,減少重複程式碼。
實際應用場景
| 場景 | 使用的 Response 類別 | 為什麼適合 |
|---|---|---|
| 健康檢查 (Health Check) | PlainTextResponse |
簡單、低開銷,客戶端只需判斷文字是否為 pong。 |
| 前端單頁應用 (SPA) 的入口頁 | HTMLResponse |
直接回傳已渲染好的 HTML,讓瀏覽器立即載入 Vue/React。 |
| RESTful API 回傳資料 | JSONResponse |
標準化的 JSON 結構,易於前端解析與文件生成。 |
| 下載檔案時的狀態訊息 | PlainTextResponse (或 JSONResponse 搭配 Content-Disposition) |
客戶端可直接顯示文字或在錯誤時提供 JSON 錯誤碼。 |
| 多語系網站的 SEO 頁面 | HTMLResponse + Jinja2 |
透過模板渲染動態 meta 標籤,提高搜尋引擎可見度。 |
| 即時資料串流 | StreamingResponse (非本文主題,但可與 JSONResponse 結合) |
大量資料不適合一次性序列化。 |
總結
本單元帶你從 基礎的自動回傳,深入到 手動控制回應類型 的三大工具:JSONResponse、HTMLResponse、PlainTextResponse。掌握這些回應類別,你將能:
- 精準控制 Content‑Type、狀態碼與標頭,提升 API 的可讀性與可維護性。
- 在同一個 FastAPI 應用中同時提供 API 與網頁,減少部署與維護成本。
- 依據不同業務需求選擇最適合的回應方式,從健康檢查到 SEO 頁面皆能輕鬆因應。
記得在實務開發中,統一回應結構、適當使用 Middleware,以及根據資料量選擇序列化工具,都是提升效能與可讀性的關鍵。祝你在 FastAPI 的旅程中,寫出更乾淨、彈性十足的服務! 🚀