π Updating an Existing Todo (PUT Request)
In the previous lesson, we implemented Create (POST).
Now we implement the third core database operation:
Update β modifying an existing record using the PUT HTTP method
Understanding this lesson is critical because it teaches how:
SQLAlchemy tracks objects in memory
Database sessions manage state
FastAPI handles validation and dependency injection
ποΈ Step 1: Define the PUT Endpoint
Now we create the endpoint below our GET and POST routes.
ποΈ main.py
@app.put("/todo/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
async def update_todo(
db: db_dependency,
todo_request: TodoRequest,
todo_id: int = Path(gt=0)
):
passWhat This Endpoint Receives:
dbβ Injected database sessiontodo_requestβ Validated request body (Pydantic model)todo_idβ Path parameter (must be greater than 0)
π Step 2: Retrieve the Existing Todo
Before updating anything, we must check whether the record exists.
ποΈ main.py
todo_model = db.query(Todos).filter(Todos.id == todo_id).first()Backend Breakdown
db.query(Todos)β Build a SELECT query.filter(Todos.id == todo_id)β Add WHERE clause.first()β Return first matching record orNone
β οΈ Since id is a primary key, only one record should match.
β Step 3: Handle Missing Records
If the Todo does not exist, we must stop execution.
ποΈ main.py
if todo_model is None:
raise HTTPException(status_code=404, detail="Todo not found.")Why this matters:
Prevents updating non-existent data
Keeps API predictable
Follows RESTful standards
βοΈ Step 4: Update the Existing Model
Now we modify the fields of the retrieved object.
ποΈ main.py
todo_model.title = todo_request.title
todo_model.description = todo_request.description
todo_model.priority = todo_request.priority
todo_model.complete = todo_request.completeπ§ Critical Backend Concept
We are modifying the existing object retrieved from the session.
Why this is important:
If we did this instead:
ποΈ main.py
new_todo = Todos(**todo_request.model_dump())SQLAlchemy would treat it as a new record, not an update.
But because we modify todo_model:
SQLAlchemy tracks the object in memory
It detects attribute changes
It automatically prepares an
UPDATESQL statement
This is called Unit of Work Pattern in SQLAlchemy.
πΎ Step 5: Commit the Transaction
After modifying the object:
ποΈ main.py
db.commit()Thatβs it.
We do NOT call db.add() again because:
The object is already attached to the session
SQLAlchemy is already tracking it
Without commit(), nothing is saved.
βοΈParameter Order Matters
The correct function signature order is:
ποΈ main.py
async def update_todo(
db: db_dependency,
todo_request: TodoRequest,
todo_id: int = Path(gt=0)
):FastAPI processes parameters in this logical order:
Dependencies
Request body models
Path parameters with defaults
Changing the order can cause validation issues.
π§© Final Clean Implementation
@app.put("/todo/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
async def update_todo(
db: db_dependency,
todo_request: TodoRequest,
todo_id: int = Path(gt=0)):
todo_model = db.query(Todos).filter(Todos.id == todo_id).first()
if todo_model is None:
raise HTTPException(status_code=404, detail="Todo not found.")
todo_model.title = todo_request.title
todo_model.description = todo_request.description
todo_model.priority = todo_request.priority
todo_model.complete = todo_request.complete
db.commit()π§ͺ Testing the Endpoint
Start your server:
uvicorn main:app --reloadOpen:
/docsYou should now see:
GET
/GET
/todo/{todo_id}POST
/todoPUT
/todo/{todo_id}
Test Scenarios
1οΈβ£ Update existing ID β 204 No Content
2οΈβ£ Non-existing ID β 404 Not Found
3οΈβ£ Negative ID β 422 Validation Error
ποΈ main.py
from typing import Annotated
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from fastapi import FastAPI, Path, Depends, HTTPException
from starlette import status
import models
from models import Todos
from database import engine, SessionLocal
app = FastAPI()
models.Base.metadata.create_all(bind=engine)
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
db_dependency = Annotated[Session, Depends(get_db)]
class TodoRequest(BaseModel):
title: str = Field(min_length=3)
description: str = Field(min_length=3, max_length=100)
priority: int = Field(gt=0, lt=6)
complete: bool
@app.get("/", status_code=status.HTTP_200_OK)
async def read_all(db: db_dependency):
return db.query(Todos).all()
@app.get("/todo/{todo_id}", status_code=status.HTTP_200_OK)
async def read_todo(db: db_dependency, todo_id: int = Path(gt=0)):
todo_model = db.query(Todos).filter(Todos.id == todo_id).first()
if todo_model is not None:
return todo_model
else:
raise HTTPException(status_code=404, detail='Todo Not Found')
@app.post("/todo/", status_code=status.HTTP_201_CREATED)
async def create_todo(db: db_dependency, todo_request: TodoRequest):
todo_model = Todos(**todo_request.model_dump())
db.add(todo_model)
db.commit()
@app.put("/todo/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
async def update_todo(
db: db_dependency,
todo_request: TodoRequest,
todo_id: int = Path(gt=0)):
todo_model = db.query(Todos).filter(Todos.id == todo_id).first()
if todo_model is None:
raise HTTPException(status_code=404, detail="Todo not found.")
todo_model.title = todo_request.title
todo_model.description = todo_request.description
todo_model.priority = todo_request.priority
todo_model.complete = todo_request.complete
db.commit()No Changes in models.py and database.py