FastAPI - Beginner to Pro / API Request Methods

Put Request (Todo Project)

πŸ”„ 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)
):
    pass

What This Endpoint Receives:

  • db β†’ Injected database session

  • todo_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 or None

⚠️ 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 UPDATE SQL 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:

  1. Dependencies

  2. Request body models

  3. 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 --reload

Open:

/docs

You should now see:

  • GET /

  • GET /todo/{todo_id}

  • POST /todo

  • PUT /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