# 

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

&gt; ⏱️ **閱讀時間：** 15 分鐘
&gt; 🎯 **難度：** ⭐ (入門)

---

## 🤔 一句話解釋

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

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

---

## 📍 路徑參數 (Path Parameters)

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

### 基本用法

```python
from fastapi import FastAPI

app = FastAPI()

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

**請求：**
```
GET /users/123
```

**回應：**
```json
{&#34;user_id&#34;: 123}
```

### 型別自動轉換

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

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

**測試：**

```bash
# 正確 - 會轉成 int
GET /items/42
# 回應: {&#34;item_id&#34;: 42, &#34;type&#34;: &#34;int&#34;}

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

### 多個路徑參數

```python
@app.get(&#34;/users/{user_id}/posts/{post_id}&#34;)
async def get_user_post(user_id: int, post_id: int):
    return {
        &#34;user_id&#34;: user_id,
        &#34;post_id&#34;: post_id
    }
```

**請求：**
```
GET /users/123/posts/456
```

### 路徑參數順序很重要！

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

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

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

**正確做法：**

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

@app.get(&#34;/users/{user_id}&#34;)
async def get_user(user_id: int):
    return {&#34;user_id&#34;: user_id}
```

---

## 🔍 查詢參數 (Query Parameters)

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

### 基本用法

```python
from fastapi import FastAPI

app = FastAPI()

@app.get(&#34;/items&#34;)
async def list_items(skip: int = 0, limit: int = 10):
    return {
        &#34;skip&#34;: skip,
        &#34;limit&#34;: limit
    }
```

**請求：**

```bash
# 使用預設值
GET /items
# 回應: {&#34;skip&#34;: 0, &#34;limit&#34;: 10}

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

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

### 必填 vs 選填

```python
from typing import Optional

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

**測試：**

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

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

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

### 布林值參數

```python
@app.get(&#34;/items&#34;)
async def list_items(
    include_deleted: bool = False
):
    return {&#34;include_deleted&#34;: include_deleted}
```

**FastAPI 支援多種布林值格式：**

```bash
# 以下都會被解析為 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
```

### 列表參數

```python
from typing import List

@app.get(&#34;/items&#34;)
async def list_items(tags: List[str] = []):
    return {&#34;tags&#34;: tags}
```

**請求：**

```bash
GET /items?tags=python&amp;tags=fastapi&amp;tags=api
# 回應: {&#34;tags&#34;: [&#34;python&#34;, &#34;fastapi&#34;, &#34;api&#34;]}
```

---

## 🎛️ 使用 Query 進行驗證

`Query` 類別提供更多驗證選項：

```python
from fastapi import FastAPI, Query

app = FastAPI()

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

### Query 常用參數

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

### 必填查詢參數

```python
from fastapi import Query

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

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

### 參數別名

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

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

**請求：**
```bash
GET /items?item-query=test
# 回應: {&#34;query&#34;: &#34;test&#34;}
```

---

## 🎛️ 使用 Path 進行驗證

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

```python
from fastapi import FastAPI, Path

app = FastAPI()

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

### 參數順序問題

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

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

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

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

---

## 🔀 路徑參數 &#43; 查詢參數

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

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

app = FastAPI()

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

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

**請求範例：**

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

**回應：**

```json
{
    &#34;user_id&#34;: 123,
    &#34;skip&#34;: 10,
    &#34;limit&#34;: 20,
    &#34;status&#34;: &#34;published&#34;,
    &#34;tags&#34;: [&#34;python&#34;, &#34;fastapi&#34;]
}
```

---

## 🎨 實戰範例：商品搜尋 API

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

app = FastAPI()

class SortOrder(str, Enum):
    asc = &#34;asc&#34;
    desc = &#34;desc&#34;

class Category(str, Enum):
    electronics = &#34;electronics&#34;
    clothing = &#34;clothing&#34;
    food = &#34;food&#34;
    books = &#34;books&#34;

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

    # 分類過濾（使用 Enum）
    category: Optional[Category] = Query(
        None,
        description=&#34;商品分類&#34;
    ),

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

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

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

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

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


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

---

## ✅ 重點總結

### 路徑參數 vs 查詢參數

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

### 何時用哪個？

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

### 驗證工具

```python
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: 路徑參數和查詢參數有什麼區別？何時使用？

**答案：**

&gt; **路徑參數**用於識別特定資源，是 URL 的一部分，通常是必填的。例如 `/users/123` 中的 `123` 用來識別特定使用者。
&gt;
&gt; **查詢參數**用於過濾、排序、分頁等操作，在 URL 的 `?` 後面，通常是選填的。例如 `/users?role=admin&amp;page=1`。
&gt;
&gt; **選擇原則：**
&gt; - 如果少了這個參數，這個 URL 就沒有意義 → 路徑參數
&gt; - 如果這個參數是用來修飾/過濾結果 → 查詢參數

---

## 🤓 小測驗

1. `/users/{id}` 中的 `{id}` 是什麼？
   &lt;details&gt;
   &lt;summary&gt;點擊看答案&lt;/summary&gt;
   路徑參數
   &lt;/details&gt;

2. `/items?page=1&amp;limit=10` 中的 `page` 和 `limit` 是什麼？
   &lt;details&gt;
   &lt;summary&gt;點擊看答案&lt;/summary&gt;
   查詢參數
   &lt;/details&gt;

3. 如何讓查詢參數變成必填？
   &lt;details&gt;
   &lt;summary&gt;點擊看答案&lt;/summary&gt;
   不給預設值，或使用 Query(...)
   &lt;/details&gt;

4. 如何限制數字參數必須大於 0？
   &lt;details&gt;
   &lt;summary&gt;點擊看答案&lt;/summary&gt;
   使用 Query(gt=0) 或 Path(gt=0)
   &lt;/details&gt;

---

## 🏋️ 練習作業

建立一個「電影搜尋 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（必須 &gt; 0）
   - `include_cast`: 是否包含演員資訊

---

**上一篇：** [01-2. 環境設定與第一個 API](./01-2)
**下一篇：** [01-4. 請求體與回應模型](./01-4)

---

最後更新：2025-12-17


---

> 作者: luk  
> URL: https://yoru-karu-blog-lalaluk-52581ac5e0cef170a3c8922c19182ecb6f7bd604.gitlab.io/posts/tutorial/fastapi/01-3/  

