常見的連線方法
以下以「待辦事項 Todo」為例,說明 HTTP 方法與 FastAPI 的處理方式。
## 一、GET、POST、PUT、PATCH、DELETE 的概念
| 方法 | 用途 | 特性 |
|---|---|---|
| `GET` | 取得資料 | 不應修改伺服器資料 |
| `POST` | 新增資料或執行動作 | 通常不是冪等操作,每次呼叫可能新增一筆 |
| `PUT` | 完整更新資料 | 通常代表以新資料取代原有資源,具冪等性 |
| `PATCH` | 部分更新資料 | 只修改指定欄位 |
| `DELETE` | 刪除資料 | 刪除指定資源,通常具冪等性 |
### 範例
假設資源網址為:
```text
/api/todos/1
```
#### GET
```http
GET /api/todos/1
```
取得編號為 `1` 的待辦事項。
#### POST
```http
POST /api/todos
Content-Type: application/json
{
"title": "學習 FastAPI",
"completed": false
}
```
新增一筆待辦事項,伺服器通常會產生新的 `id`。
#### PUT
```http
PUT /api/todos/1
Content-Type: application/json
{
"title": "完整修改標題",
"completed": true
}
```
以完整內容取代編號 `1` 的資料。
#### PATCH
```http
PATCH /api/todos/1
Content-Type: application/json
{
"completed": true
}
```
只修改 `completed` 欄位,其他欄位保持不變。
#### DELETE
```http
DELETE /api/todos/1
```
刪除編號為 `1` 的待辦事項。
---
# 二、FastAPI 如何處理不同 HTTP 方法
FastAPI 使用「路由裝飾器」將 URL 與 HTTP 方法對應到 Python 函式。
```python
from fastapi import FastAPI
app = FastAPI()
@app.get("/todos")
def get_todos():
return {"message": "取得所有待辦事項"}
@app.post("/todos")
def create_todo():
return {"message": "新增待辦事項"}
@app.put("/todos/{todo_id}")
def replace_todo(todo_id: int):
return {"message": f"完整更新 Todo {todo_id}"}
@app.patch("/todos/{todo_id}")
def update_todo(todo_id: int):
return {"message": f"部分更新 Todo {todo_id}"}
@app.delete("/todos/{todo_id}")
def delete_todo(todo_id: int):
return {"message": f"刪除 Todo {todo_id}"}
```
其中:
```python
@app.get("/todos")
```
表示當使用者以 `GET /todos` 存取時,執行下面的函式。
```python
@app.patch("/todos/{todo_id}")
```
其中 `{todo_id}` 是路徑參數,例如:
```text
PATCH /todos/3
```
FastAPI 會自動將 `3` 傳入函式的 `todo_id` 參數。
---
# 三、完整 FastAPI 範例
## 1. 安裝套件
```bash
pip install fastapi uvicorn
```
## 2. 建立 `main.py`
```python
from typing import Optional
from fastapi import FastAPI, HTTPException, Response, status
from pydantic import BaseModel
app = FastAPI(title="Todo API")
# 完整新增或更新時使用
class TodoCreate(BaseModel):
title: str
completed: bool = False
# PATCH 使用,所有欄位都是可選的
class TodoUpdate(BaseModel):
title: Optional[str] = None
completed: Optional[bool] = None
# 模擬資料庫
todos = {
1: {
"id": 1,
"title": "學習 FastAPI",
"completed": False
},
2: {
"id": 2,
"title": "學習 fetch",
"completed": False
}
}
next_id = 3
# GET:取得所有待辦事項
@app.get("/api/todos")
def get_todos():
return list(todos.values())
# GET:取得單一待辦事項
@app.get("/api/todos/{todo_id}")
def get_todo(todo_id: int):
todo = todos.get(todo_id)
if todo is None:
raise HTTPException(
status_code=404,
detail="找不到此待辦事項"
)
return todo
# POST:新增待辦事項
@app.post("/api/todos", status_code=201)
def create_todo(todo_data: TodoCreate):
global next_id
new_todo = {
"id": next_id,
"title": todo_data.title,
"completed": todo_data.completed
}
todos[next_id] = new_todo
next_id += 1
return new_todo
# PUT:完整取代一筆待辦事項
@app.put("/api/todos/{todo_id}")
def replace_todo(todo_id: int, todo_data: TodoCreate):
if todo_id not in todos:
raise HTTPException(
status_code=404,
detail="找不到此待辦事項"
)
todos[todo_id] = {
"id": todo_id,
"title": todo_data.title,
"completed": todo_data.completed
}
return todos[todo_id]
# PATCH:部分更新一筆待辦事項
@app.patch("/api/todos/{todo_id}")
def update_todo(todo_id: int, todo_data: TodoUpdate):
if todo_id not in todos:
raise HTTPException(
status_code=404,
detail="找不到此待辦事項"
)
# exclude_unset=True:
# 只取得請求中實際送出的欄位
update_data = todo_data.model_dump(exclude_unset=True)
todos[todo_id].update(update_data)
return todos[todo_id]
# DELETE:刪除待辦事項
@app.delete("/api/todos/{todo_id}", status_code=204)
def delete_todo(todo_id: int):
if todo_id not in todos:
raise HTTPException(
status_code=404,
detail="找不到此待辦事項"
)
del todos[todo_id]
# 204 No Content 不應該回傳 JSON 內容
return Response(status_code=status.HTTP_204_NO_CONTENT)
```
啟動伺服器:
```bash
uvicorn main:app --reload
```
啟動後可以開啟:
```text
http://127.0.0.1:8000/docs
```
FastAPI 會自動產生 Swagger API 測試介面。
---
# 四、前端使用 `fetch()` 呼叫 API
建立 `index.html`:
```html
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>Todo API 範例</title>
</head>
<body>
<h1>Todo 清單</h1>
<input id="titleInput" type="text" placeholder="輸入待辦事項">
<button onclick="createTodo()">新增</button>
<button onclick="loadTodos()">重新載入</button>
<ul id="todoList"></ul>
<script>
const API_URL = "http://127.0.0.1:8000/api/todos";
// GET:取得所有資料
async function loadTodos() {
const response = await fetch(API_URL);
if (!response.ok) {
alert("取得資料失敗");
return;
}
const todos = await response.json();
renderTodos(todos);
}
// 將資料顯示在畫面上
function renderTodos(todos) {
const list = document.querySelector("#todoList");
list.innerHTML = "";
todos.forEach(todo => {
const li = document.createElement("li");
li.innerHTML = `
<span>
${todo.id}. ${todo.title}
${todo.completed ? "✅" : "⬜"}
</span>
<button onclick="toggleTodo(${todo.id}, ${todo.completed})">
PATCH 完成狀態
</button>
<button onclick="replaceTodo(${todo.id})">
PUT 完整更新
</button>
<button onclick="deleteTodo(${todo.id})">
DELETE
</button>
`;
list.appendChild(li);
});
}
// POST:新增資料
async function createTodo() {
const input = document.querySelector("#titleInput");
const title = input.value.trim();
if (!title) {
alert("請輸入待辦事項");
return;
}
const response = await fetch(API_URL, {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
title: title,
completed: false
})
});
if (!response.ok) {
alert("新增失敗");
return;
}
input.value = "";
await loadTodos();
}
// PATCH:只修改 completed 欄位
async function toggleTodo(todoId, currentCompleted) {
const response = await fetch(`${API_URL}/${todoId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
completed: !currentCompleted
})
});
if (!response.ok) {
alert("部分更新失敗");
return;
}
await loadTodos();
}
// PUT:完整取代資料
async function replaceTodo(todoId) {
const newTitle = prompt("請輸入新的標題:");
if (!newTitle) {
return;
}
const response = await fetch(`${API_URL}/${todoId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
title: newTitle,
completed: false
})
});
if (!response.ok) {
alert("完整更新失敗");
return;
}
await loadTodos();
}
// DELETE:刪除資料
async function deleteTodo(todoId) {
const confirmed = confirm("確定要刪除嗎?");
if (!confirmed) {
return;
}
const response = await fetch(`${API_URL}/${todoId}`, {
method: "DELETE"
});
if (!response.ok) {
alert("刪除失敗");
return;
}
await loadTodos();
}
// 頁面載入時先取得資料
loadTodos();
</script>
</body>
</html>
```
---
# 五、`fetch()` 的基本格式
## GET
```javascript
const response = await fetch("/api/todos");
const data = await response.json();
```
## POST
```javascript
const response = await fetch("/api/todos", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
title: "新的待辦事項",
completed: false
})
});
```
## PUT
```javascript
await fetch("/api/todos/1", {
method: "PUT",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
title: "完整更新後的標題",
completed: true
})
});
```
## PATCH
```javascript
await fetch("/api/todos/1", {
method: "PATCH",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
completed: true
})
});
```
## DELETE
```javascript
await fetch("/api/todos/1", {
method: "DELETE"
});
```
---
# 六、跨來源請求與 CORS
如果前端和 FastAPI 使用不同來源,例如:
```text
前端:http://127.0.0.1:5500
後端:http://127.0.0.1:8000
```
瀏覽器可能會阻擋請求,此時 FastAPI 需要設定 CORS。
在 `main.py` 加入:
```python
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://127.0.0.1:5500"
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
```
開發階段也可以暫時允許所有來源:
```python
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
```
正式環境則應限制 `allow_origins`,不要直接允許所有來源。
簡單來說:
- `GET`:讀取資料
- `POST`:新增資料
- `PUT`:完整取代資料
- `PATCH`:部分修改資料
- `DELETE`:刪除資料
- FastAPI 使用 `@app.get()`、`@app.post()` 等裝飾器處理不同方法
- 前端則透過 `fetch()` 指定 `method`、`headers` 與 `body` 來呼叫 API
相關學習地圖、教學課程
Python 後端工程、資料庫