HTTP Methods in FastAPI
FastAPI provides a decorator for every standard HTTP method. Each maps directly to a CRUD operation:
| Method | Decorator | CRUD | Idempotent | Request Body |
|---|---|---|---|---|
| GET | @app.get() | Read | Yes | No |
| POST | @app.post() | Create | No | Yes |
| PUT | @app.put() | Replace | Yes | Yes |
| PATCH | @app.patch() | Partial Update | No | Yes |
| DELETE | @app.delete() | Delete | Yes | Optional |
Building a CRUD API with FastAPI
Let’s build a task management API that demonstrates every HTTP method:
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
from typing import Optional
app = FastAPI(title="Task Manager API")
# ── Pydantic Models ──
class TaskBase(BaseModel):
title: str
completed: bool = False
class TaskCreate(TaskBase):
pass
class TaskUpdate(BaseModel):
title: Optional[str] = None
completed: Optional[bool] = None
class Task(TaskBase):
id: int
# ── In-Memory Store ──
tasks_db = {}
next_id = 1
GET - Retrieve Resources
@app.get("/tasks", response_model=list[Task])
def list_tasks(completed: Optional[bool] = None):
"""Return all tasks, optionally filtered by completion status."""
if completed is None:
return list(tasks_db.values())
return [t for t in tasks_db.values() if t.completed == completed]
@app.get("/tasks/{task_id}", response_model=Task)
def get_task(task_id: int):
"""Return a single task by ID."""
task = tasks_db.get(task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return task
Status codes for GET:
| Scenario | Code |
|---|---|
| Resource found | 200 OK |
| Resource not found | 404 Not Found |
| Invalid ID format | 422 Unprocessable Entity |
POST - Create Resources
Use status_code to signal 201 Created:
@app.post("/tasks", response_model=Task, status_code=status.HTTP_201_CREATED)
def create_task(payload: TaskCreate):
global next_id
task = Task(id=next_id, **payload.model_dump())
tasks_db[next_id] = task
next_id += 1
return task
curl -X POST http://localhost:8000/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Learn FastAPI"}'
# 201 Created
# {"id":1,"title":"Learn FastAPI","completed":false}
| Scenario | Code |
|---|---|
| Resource created | 201 Created |
| Validation failure | 422 Unprocessable Entity |
PUT - Full Replacement
PUT replaces the entire resource. Missing fields get reset:
@app.put("/tasks/{task_id}", response_model=Task)
def replace_task(task_id: int, payload: TaskCreate):
"""Fully replace a task - requires all fields."""
if task_id not in tasks_db:
raise HTTPException(status_code=404, detail="Task not found")
task = Task(id=task_id, **payload.model_dump())
tasks_db[task_id] = task
return task
curl -X PUT http://localhost:8000/tasks/1 \
-H "Content-Type: application/json" \
-d '{"title": "Master FastAPI", "completed": true}'
PUT vs PATCH: PUT replaces the whole resource. If you omit
completed, it resets to the model default (False). Use PATCH for partial updates.
PATCH - Partial Update
PATCH only modifies the fields you send:
@app.patch("/tasks/{task_id}", response_model=Task)
def patch_task(task_id: int, payload: TaskUpdate):
"""Partial update - only supplied fields change."""
if task_id not in tasks_db:
raise HTTPException(status_code=404, detail="Task not found")
existing = tasks_db[task_id]
update_data = payload.model_dump(exclude_unset=True)
updated = existing.model_copy(update=update_data)
tasks_db[task_id] = updated
return updated
exclude_unset=True ensures only the fields the client actually sent are applied. Fields with None values in TaskUpdate are untouched.
# Only update the title, leave completed as-is
curl -X PATCH http://localhost:8000/tasks/1 \
-H "Content-Type: application/json" \
-d '{"title": "FastAPI Advanced"}'
DELETE - Remove Resources
@app.delete("/tasks/{task_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_task(task_id: int):
"""Delete a task. Returns 204 No Content on success."""
if task_id not in tasks_db:
raise HTTPException(status_code=404, detail="Task not found")
del tasks_db[task_id]
curl -X DELETE http://localhost:8000/tasks/1
# 204 No Content (empty body)
curl -X DELETE http://localhost:8000/tasks/999
# 404 Not Found
# {"detail":"Task not found"}
| Scenario | Code |
|---|---|
| Deleted successfully | 204 No Content |
| Resource not found | 404 Not Found |
The response_model Parameter
FastAPI uses response_model to filter and document the response:
@app.get("/tasks", response_model=list[Task], response_model_exclude_unset=True)
def list_tasks():
...
| Parameter | Effect |
|---|---|
response_model | Schema for serialization & OpenAPI docs |
response_model_exclude_unset | Omit default values from response |
response_model_include | Only include specific fields |
response_model_exclude | Exclude specific fields |
@app.get("/tasks/{task_id}", response_model=Task, response_model_include={"id", "title"})
def get_task(task_id: int):
"""Only returns id and title - no completed field."""
...
Generic Route with @app.api()
Use @app.api() when you need to register the same handler under multiple methods:
@app.api_route("/tasks/{task_id}", methods=["GET", "DELETE"])
def handle_task(task_id: int, method: str = "GET"):
if method == "DELETE":
if task_id not in tasks_db:
raise HTTPException(status_code=404)
del tasks_db[task_id]
return None
task = tasks_db.get(task_id)
if not task:
raise HTTPException(status_code=404)
return task
Choosing the Right Status Code
Client ──GET─────► 200 OK
Client ──POST────► 201 Created
Client ──PUT─────► 200 OK
Client ──PATCH───► 200 OK
Client ──DELETE──► 204 No Content
│
▼
Validation Error ──► 422
Not Found ──► 404
Key Takeaways
- FastAPI provides
@app.get(),@app.post(),@app.put(),@app.patch(),@app.delete()- one for each HTTP method - Use
status_code=status.HTTP_201_CREATEDfor POST andstatus.HTTP_204_NO_CONTENTfor DELETE response_modelcontrols serialization, filtering, and OpenAPI schema generation- PUT replaces the entire resource; PATCH only modifies sent fields (use
exclude_unset=True) - Always return meaningful HTTP status codes - 200, 201, 204, 404, 422
- Raise
HTTPExceptionfor error responses instead of returning dicts manually