01-3. 路徑參數與查詢參數

⏱️ 閱讀時間: 15 分鐘 🎯 難度: ⭐ (入門)


🤔 一句話解釋

路徑參數是 URL 的一部分,查詢參數是 URL 後面的 ?key=value

https://api.example.com/users/123/posts?page=1&limit=10
                            ↑            ↑         ↑
                        路徑參數      查詢參數   查詢參數

📍 路徑參數 (Path Parameters)

路徑參數用來識別特定的資源。

基本用法

from fastapi import FastAPI

app = FastAPI()

# {user_id} 就是路徑參數
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    return {"user_id": user_id}

請求:

GET /users/123

回應:

{"user_id": 123}

型別自動轉換

FastAPI 會自動根據型別提示轉換參數:

@app.get("/items/{item_id}")
async def get_item(item_id: int):  # 自動轉成 int
    return {"item_id": item_id, "type": type(item_id).__name__}

測試:

# 正確 - 會轉成 int
GET /items/42
# 回應: {"item_id": 42, "type": "int"}

# 錯誤 - 無法轉換
GET /items/abc
# 回應: 422 Unprocessable Entity
# {
#     "detail": [{
#         "loc": ["path", "item_id"],
#         "msg": "Input should be a valid integer",
#         "type": "int_parsing"
#     }]
# }

多個路徑參數

@app.get("/users/{user_id}/posts/{post_id}")
async def get_user_post(user_id: int, post_id: int):
    return {
        "user_id": user_id,
        "post_id": post_id
    }

請求:

GET /users/123/posts/456

路徑參數順序很重要!

# ⚠️ 順序錯誤的例子
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    return {"user_id": user_id}

@app.get("/users/me")  # 這個永遠不會被匹配到!
async def get_current_user():
    return {"user": "current"}

問題: /users/me 會被第一個路由匹配,me 會嘗試轉成 int 然後失敗。

正確做法:

# ✅ 固定路徑放前面
@app.get("/users/me")
async def get_current_user():
    return {"user": "current"}

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    return {"user_id": user_id}

🔍 查詢參數 (Query Parameters)

查詢參數用於過濾、排序、分頁等。

基本用法

from fastapi import FastAPI

app = FastAPI()

@app.get("/items")
async def list_items(skip: int = 0, limit: int = 10):
    return {
        "skip": skip,
        "limit": limit
    }

請求:

# 使用預設值
GET /items
# 回應: {"skip": 0, "limit": 10}

# 指定參數
GET /items?skip=20&limit=50
# 回應: {"skip": 20, "limit": 50}

# 只指定部分參數
GET /items?limit=5
# 回應: {"skip": 0, "limit": 5}

必填 vs 選填

from typing import Optional

@app.get("/search")
async def search(
    q: str,                    # 必填(沒有預設值)
    page: int = 1,             # 選填(有預設值)
    size: int = 10,            # 選填
    sort: Optional[str] = None # 選填(明確標記可為 None)
):
    return {
        "query": q,
        "page": page,
        "size": size,
        "sort": sort
    }

測試:

# 缺少必填參數
GET /search
# 回應: 422 Unprocessable Entity
# {"detail": [{"loc": ["query", "q"], "msg": "Field required"}]}

# 正確請求
GET /search?q=fastapi
# 回應: {"query": "fastapi", "page": 1, "size": 10, "sort": null}

# 完整請求
GET /search?q=fastapi&page=2&size=20&sort=date
# 回應: {"query": "fastapi", "page": 2, "size": 20, "sort": "date"}

布林值參數

@app.get("/items")
async def list_items(
    include_deleted: bool = False
):
    return {"include_deleted": include_deleted}

FastAPI 支援多種布林值格式:

# 以下都會被解析為 True
GET /items?include_deleted=true
GET /items?include_deleted=True
GET /items?include_deleted=1
GET /items?include_deleted=yes
GET /items?include_deleted=on

# 以下都會被解析為 False
GET /items?include_deleted=false
GET /items?include_deleted=False
GET /items?include_deleted=0
GET /items?include_deleted=no
GET /items?include_deleted=off

列表參數

from typing import List

@app.get("/items")
async def list_items(tags: List[str] = []):
    return {"tags": tags}

請求:

GET /items?tags=python&tags=fastapi&tags=api
# 回應: {"tags": ["python", "fastapi", "api"]}

🎛️ 使用 Query 進行驗證

Query 類別提供更多驗證選項:

from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/items")
async def list_items(
    # 字串驗證
    q: str = Query(
        default=None,
        min_length=3,
        max_length=50,
        pattern="^[a-zA-Z]+$",  # 正規表達式
        title="搜尋關鍵字",
        description="用於搜尋的關鍵字,只能包含英文字母"
    ),
    # 數字驗證
    page: int = Query(
        default=1,
        ge=1,          # >= 1
        le=100,        # <= 100
        title="頁碼",
        description="第幾頁,從 1 開始"
    ),
    size: int = Query(
        default=10,
        gt=0,          # > 0
        lt=101,        # < 101
        title="每頁數量"
    ),
):
    return {"q": q, "page": page, "size": size}

Query 常用參數

參數說明範例
default預設值default=10
min_length最小長度min_length=3
max_length最大長度max_length=50
pattern正規表達式pattern="^[a-z]+$"
ge大於等於ge=1
gt大於gt=0
le小於等於le=100
lt小於lt=101
title標題(文件用)title="頁碼"
description描述(文件用)description="..."
deprecated標記為棄用deprecated=True
alias參數別名alias="item-query"

必填查詢參數

from fastapi import Query

@app.get("/search")
async def search(
    # 方法 1: 使用 ... (Ellipsis)
    q: str = Query(..., min_length=1),

    # 方法 2: 使用 Query(default=...)
    # q: str = Query(default=..., min_length=1),
):
    return {"query": q}

參數別名

當 Python 變數名稱與實際參數名稱不同時:

@app.get("/items")
async def list_items(
    # URL 用 item-query,程式碼用 item_query
    item_query: str = Query(default=None, alias="item-query")
):
    return {"query": item_query}

請求:

GET /items?item-query=test
# 回應: {"query": "test"}

🎛️ 使用 Path 進行驗證

Path 類別用於路徑參數的驗證:

from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/items/{item_id}")
async def get_item(
    item_id: int = Path(
        ...,           # 必填
        ge=1,          # >= 1
        le=1000,       # <= 1000
        title="項目 ID",
        description="項目的唯一識別碼"
    )
):
    return {"item_id": item_id}

參數順序問題

Python 要求有預設值的參數必須在沒有預設值的參數後面:

# ❌ 錯誤:有預設值的參數放前面
@app.get("/items/{item_id}")
async def get_item(
    q: str = None,  # 有預設值
    item_id: int    # 沒有預設值 → SyntaxError!
):
    pass

# ✅ 正確:使用 * 讓順序無所謂
@app.get("/items/{item_id}")
async def get_item(
    *,                      # 之後的都是 keyword-only
    item_id: int,           # 可以放前面
    q: str = None
):
    return {"item_id": item_id, "q": q}

# ✅ 正確:使用 Path() 並給預設值
@app.get("/items/{item_id}")
async def get_item(
    q: str = None,
    item_id: int = Path(..., ge=1)  # ... 表示必填
):
    return {"item_id": item_id, "q": q}

🔀 路徑參數 + 查詢參數

實際應用中常常混合使用:

from fastapi import FastAPI, Path, Query
from typing import Optional

app = FastAPI()

@app.get("/users/{user_id}/posts")
async def get_user_posts(
    # 路徑參數
    user_id: int = Path(..., ge=1, description="使用者 ID"),

    # 查詢參數
    skip: int = Query(0, ge=0, description="跳過的數量"),
    limit: int = Query(10, ge=1, le=100, description="返回的數量"),
    status: Optional[str] = Query(None, description="文章狀態過濾"),
    tags: list[str] = Query([], description="標籤過濾"),
):
    return {
        "user_id": user_id,
        "skip": skip,
        "limit": limit,
        "status": status,
        "tags": tags
    }

請求範例:

GET /users/123/posts?skip=10&limit=20&status=published&tags=python&tags=fastapi

回應:

{
    "user_id": 123,
    "skip": 10,
    "limit": 20,
    "status": "published",
    "tags": ["python", "fastapi"]
}

🎨 實戰範例:商品搜尋 API

from fastapi import FastAPI, Query, Path
from typing import Optional
from enum import Enum

app = FastAPI()

class SortOrder(str, Enum):
    asc = "asc"
    desc = "desc"

class Category(str, Enum):
    electronics = "electronics"
    clothing = "clothing"
    food = "food"
    books = "books"

@app.get("/products")
async def search_products(
    # 搜尋關鍵字
    q: Optional[str] = Query(
        None,
        min_length=2,
        max_length=100,
        description="搜尋關鍵字"
    ),

    # 分類過濾(使用 Enum)
    category: Optional[Category] = Query(
        None,
        description="商品分類"
    ),

    # 價格範圍
    min_price: float = Query(
        0,
        ge=0,
        description="最低價格"
    ),
    max_price: Optional[float] = Query(
        None,
        ge=0,
        description="最高價格"
    ),

    # 排序
    sort_by: str = Query(
        "created_at",
        description="排序欄位"
    ),
    order: SortOrder = Query(
        SortOrder.desc,
        description="排序方向"
    ),

    # 分頁
    page: int = Query(1, ge=1, description="頁碼"),
    per_page: int = Query(20, ge=1, le=100, description="每頁數量"),

    # 布林過濾
    in_stock: bool = Query(True, description="只顯示有庫存"),
    on_sale: bool = Query(False, description="只顯示特價商品"),
):
    """
    搜尋商品

    支援多種過濾和排序選項:
    - 關鍵字搜尋
    - 分類過濾
    - 價格範圍
    - 排序
    - 分頁
    """
    return {
        "filters": {
            "q": q,
            "category": category,
            "min_price": min_price,
            "max_price": max_price,
            "in_stock": in_stock,
            "on_sale": on_sale,
        },
        "sort": {
            "by": sort_by,
            "order": order
        },
        "pagination": {
            "page": page,
            "per_page": per_page,
            "total": 100,  # 假設總共 100 筆
            "pages": 5
        },
        "results": []  # 實際的搜尋結果
    }


@app.get("/products/{product_id}")
async def get_product(
    product_id: int = Path(
        ...,
        ge=1,
        title="商品 ID",
        description="商品的唯一識別碼"
    ),
    include_reviews: bool = Query(
        False,
        description="是否包含評論"
    ),
    include_related: bool = Query(
        False,
        description="是否包含相關商品"
    )
):
    """獲取單一商品詳情"""
    return {
        "product_id": product_id,
        "include_reviews": include_reviews,
        "include_related": include_related
    }

✅ 重點總結

路徑參數 vs 查詢參數

特性路徑參數查詢參數
位置URL 路徑中? 後面
用途識別資源過濾、排序、分頁
必填通常必填可選填
語法/{param}?key=value
範例/users/123/users?role=admin

何時用哪個?

┌─────────────────────────────────────────────┐
│  這個參數是用來「識別唯一資源」嗎?          │
│                                             │
│  是 → 用路徑參數                             │
│       GET /users/{user_id}                  │
│       GET /posts/{post_id}                  │
│                                             │
│  否 → 用查詢參數                             │
│       GET /users?role=admin&active=true     │
│       GET /posts?page=1&limit=10            │
└─────────────────────────────────────────────┘

驗證工具

from fastapi import Path, Query

# 路徑參數驗證
item_id: int = Path(..., ge=1, le=1000)

# 查詢參數驗證
page: int = Query(1, ge=1, le=100)
q: str = Query(..., min_length=3, max_length=50)

🎤 面試這樣答

Q: 路徑參數和查詢參數有什麼區別?何時使用?

答案:

路徑參數用於識別特定資源,是 URL 的一部分,通常是必填的。例如 /users/123 中的 123 用來識別特定使用者。

查詢參數用於過濾、排序、分頁等操作,在 URL 的 ? 後面,通常是選填的。例如 /users?role=admin&page=1

選擇原則:

  • 如果少了這個參數,這個 URL 就沒有意義 → 路徑參數
  • 如果這個參數是用來修飾/過濾結果 → 查詢參數

🤓 小測驗

  1. /users/{id} 中的 {id} 是什麼?

  2. /items?page=1&limit=10 中的 pagelimit 是什麼?

  3. 如何讓查詢參數變成必填?

  4. 如何限制數字參數必須大於 0?


🏋️ 練習作業

建立一個「電影搜尋 API」:

  1. GET /movies - 搜尋電影

    • q: 搜尋關鍵字(選填,至少 2 字)
    • genre: 類型過濾(選填)
    • year_from: 年份起始(選填)
    • year_to: 年份結束(選填)
    • sort: 排序欄位(rating, year, title)
    • page: 頁碼(預設 1)
    • limit: 每頁數量(預設 20,最大 100)
  2. GET /movies/{movie_id} - 獲取電影詳情

    • movie_id: 電影 ID(必須 > 0)
    • include_cast: 是否包含演員資訊

上一篇: 01-2. 環境設定與第一個 API 下一篇: 01-4. 請求體與回應模型


最後更新:2025-12-17

0%