FastAPI - Beginner to Pro / Authentication & Authorization

Router Scale Todos File

πŸ› οΈ Project Refactor β€” Moving Todos into a Router (Scalable Structure)

In the last session, we created our routers package and added auth.py inside it.

Our main.py file was referencing auth.py.

Now, we are going to properly modularize our project by creating a new file:

πŸ‘‰ todos.py


🧱 Step 1 β€” Create todos.py

Inside the routers package:

<aside> πŸ’‘

you can also create via the terminal using the command

cd routers

ni filename.py

</aside>

Our todos.py will need everything that auth.py has β€” meaning it will define its own router.

from fastapi import APIRouter

router = APIRouter()

Current Structure

Right now, our FastAPI folder contains the virtual environment, and inside it is our actual Python project (TodoApp).

FastAPI
 └── TodoApp

Project Structure Recommendation

To prevent module resolution and import path issues, the TodoApp directory should be opened as the root workspace in VS Code, rather than the parent FastAPI directory.

This ensures:

  • Proper Python module resolution

  • Clean absolute imports (e.g., from database import engine)

  • No ModuleNotFoundError during execution

  • Correct working directory when running uvicorn

Once in TodoApp directory, create a new virtual environment and install all dependencies for the project.

Now our TodoApp is going to be the parent location, not the FastAPI directory.


πŸ”„ Step 2 β€” Move Logic from main.py to todos.py

Now:

πŸ‘‰ Copy all endpoint logic from main.py

πŸ‘‰ Paste it inside todos.py

Then make the following changes:

❌ Remove Unnecessary Items

Remove:

models.Base.metadata.create_all(bind=engine)

app.include_router(auth.router)

Remove:

from routers import auth
import models

Remove: engine

from database import engine, SessionLocal

✏️ Replace FastAPI Instance

from fastapi import FastAPI -> from fastapi import APIRouter

app = FastAPI() -> router = APIRouter()

Change all below endpoints

@app -> @router
from typing import Annotated
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from fastapi import APIRouter, Path, Depends, HTTPException
from starlette import status
from models import Todos
from database import SessionLocal

router = APIRouter()

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

@router.get("/", status_code=status.HTTP_200_OK)
async def read_all(db: db_dependency):
    return db.query(Todos).all()

@router.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')

@router.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()
    
@router.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()
    
@router.delete("/todo/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_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 None:
        raise HTTPException(status_code=404, detail="Todo not found.")

    db.delete(todo_model)
    db.commit()

🧠 Step 3 β€” Update main.py

Now we make main.py responsible only for:

  • Creating the FastAPI app

  • Creating the database

  • Including routers

Updated main.py

from fastapi import FastAPI
import models
from database import engine
from routers import auth, todos

app = FastAPI()

models.Base.metadata.create_all(bind=engine)

app.include_router(auth.router)
app.include_router(todos.router)

πŸš€ Step 4 β€” Run the Application

uvicorn main:app --reload

🌐 Testing in Browser

  • /auth/ β†’ Returns authenticated user

  • / β†’ Returns all todos

  • /todo/{id} β†’ Returns specific todo

Everything still works β€” but now the structure is clean and scalable.


πŸ“¦ What We Achieved (Deep Understanding Section)

Instead of defining endpoints directly inside main.py, we:

πŸ‘‰ Moved them into routers

πŸ‘‰ Separated responsibilities

πŸ‘‰ Improved scalability

πŸ‘‰ Improved maintainability

Now:

  • main.py β†’ Application startup & router registration

  • auth.py β†’ Authentication logic

  • todos.py β†’ Todo logic

This structure allows us to:

  • Add more modules (users, admin, products, etc.)

  • Implement authentication & authorization cleanly

  • Scale the application properly


πŸ—‚οΈ main.py

from fastapi import FastAPI
import models
from database import engine
from routers import auth, todos

app = FastAPI()

models.Base.metadata.create_all(bind=engine)

app.include_router(auth.router)
app.include_router(todos.router)

πŸ—‚οΈ routers/todos.py

from typing import Annotated
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from fastapi import APIRouter, Path, Depends, HTTPException
from starlette import status
from models import Todos
from database import SessionLocal

router = APIRouter()

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

@router.get("/", status_code=status.HTTP_200_OK)
async def read_all(db: db_dependency):
    return db.query(Todos).all()

@router.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')

@router.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()
    
@router.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()
    
@router.delete("/todo/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_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 None:
        raise HTTPException(status_code=404, detail="Todo not found.")

    db.delete(todo_model)
    db.commit()

No Changes in Other files and Structure