FastAPI - Beginner to Pro / API Request Methods

Post Request (Todo Project)

➕ Creating a New Todo (POST Request)

In the previous lesson, we built an endpoint to retrieve a single Todo by its todo_id.

Now we move to the next core backend concept:

👉 Creating and saving a new Todo using a POST request.

To implement this properly, we need:

  • A Pydantic request model

  • Proper field validation

  • A POST endpoint

  • Clean database session handling

This is where input validation and database persistence come together.


🧾 Step 1: Creating the Pydantic Request Model

Before defining the POST endpoint, we must define the structure of the request body.

Right below our database dependency, create a new Pydantic model.

Make sure BaseModel is imported.

🗂️ main.py

from typing import Annotated
from pydantic import BaseModel
from sqlalchemy.orm import Session
from fastapi import FastAPI, Depends
from starlette import status

import models
from models import Todos
from database import SessionLocal, engine

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):
		pass 

At this stage, we’ve defined the structure — but no fields yet.


🏗️ Step 2: Defining Request Fields

Now we define what data the client must send in the request body.

🗂️ main.py

class TodoRequest(BaseModel):
    title: str
    description: str
    priority: int
    complete: bool

These fields represent the exact data required to create a Todo.


🗄️ Step 3: Comparing With the Database Model

Inside models.py, our table looks like this:

🗂️ models.py

class Todos(Base):
    __tablename__ = 'todos'

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String)
    description = Column(String)
    priority = Column(Integer)
    complete = Column(Boolean, default=False)

🔎 Important Observation

We did not include id in TodoRequest.

Why?

  • id is the primary key

  • It auto-increments

  • SQLAlchemy generates it automatically

  • The client should never control primary keys

When a new record is inserted, SQLAlchemy assigns the ID for us.

This is critical backend design practice.


✅ Step 4: Adding Field Validation

Now we improve the request model using Pydantic’s Field validation.

First, import it:

🗂️ main.py

from pydantic import BaseModel, Field

Now update the model:

🗂️ main.py

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

🔎 What These Rules Enforce

  • title → Minimum 3 characters

  • description → Between 3 and 100 characters

  • priority → Must be between 1 and 5

  • complete → Must be a boolean

If any validation fails, FastAPI automatically returns:

422 Unprocessable Entity

Validation happens before your endpoint logic runs.


🚀 Step 5: Creating the POST Endpoint

Now we can implement the POST request.

🗂️ main.py

@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()

This is the moment where data moves from request → database.

🧠 Deep Breakdown of What’s Happening

🔹 todo_request.model_dump()

Converts the validated Pydantic model into a safe dictionary.

🔹 Todos(**data)

Unpacks the dictionary to create a SQLAlchemy model instance.

🔹 db.add(todo_model)

Adds the object to the session (marks for insertion).

🔹 db.commit()

Saves the transaction permanently to the database.

Without commit(), data is not stored.

This is how SQLAlchemy transactions work internally.


🌐 Testing the Endpoint

Start the server:

uvicorn main:app --reload

Then open:

/docs

You should now see:

  • GET /

  • GET /todo/{todo_id}

  • POST /todo/

Click POST /todo/, provide a request body, and execute.

If validation passes → the Todo is saved.


🗂️ 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()

No Changes in models.py and database.py