Creating Your First Feature
This guide walks you through creating a complete backend feature from scratch: a task management system (aka the good old TODO list). You'll learn how to create database models, API endpoints and test everything together.
What We'll Build
A simple task management feature with:
- Task model with title, description, and completion status
- Full CRUD API endpoints
- User authentication
- Database migration
Prerequisites
- Starter app is set up and running
- Backend development environment is configured
- Basic understanding of Python and FastAPI
Step 1: Create the Database Model
First, let's define our task model in back/src/models/db_models.py:
# Add these imports at the top if not already present
from datetime import datetime
from uuid import UUID
from sqlmodel import Field, Relationship
# Add this new model at the end of the file
class TaskBase(ModelWithIdAndTimestamps):
title: str = Field(max_length=200)
description: str | None = Field(default=None, max_length=1000)
completed: bool = Field(default=False)
due_date: datetime | None = Field(default=None)
user_id: UUID = Field(foreign_key="user.id", index=True)
class Task(TaskBase, table=True):
user: User = Relationship(back_populates="tasks")
Update the User model to include the relationship:
# In the User class, add this line:
class User(UserBase, table=True):
group: Group | None = Relationship(back_populates="users")
tasks: list[Task] = Relationship(back_populates="user") # Add this line
Step 2: Create API Models
Add the API models in back/src/models/api_models.py:
# Add at the end of the file
CreateTaskDTO = create_create_model(TaskBase, "CreateTask")
UpdateTaskDTO = create_update_model(TaskBase, "UpdateTask")
class TaskDTO(BaseDTO, TaskBase):
pass
class TaskWithUserDTO(TaskDTO):
user: UserDTO | None = None
Step 3: Generate Database Migration
Create and apply the database migration:
cd back
uv run alembic revision --autogenerate -m "Add tasks table"
uv run alembic upgrade head
Step 4: Create the Router
Create back/src/routers/task.py:
from typing import List
from uuid import UUID
from fastapi import HTTPException
from sqlalchemy.orm import joinedload
from sqlmodel import select
from models.api_models import CreateTaskDTO, UpdateTaskDTO, TaskDTO, TaskWithUserDTO
from models.db_models import Task, User
from routers.router_utils import CRUDBaseRouter
from services.database_service import DatabaseDep
from fastapi import Depends
from utils.auth import get_current_user
# CRUD router with automatic endpoints
_task_crud: CRUDBaseRouter = CRUDBaseRouter(
model=Task,
read_schema=TaskDTO,
create_schema=CreateTaskDTO,
update_schema=UpdateTaskDTO,
prefix="/tasks",
tags=["Tasks"],
dependencies=[],
)
router = _task_crud.get_router()
# Custom endpoints for user-specific operations
@router.get("/my-tasks", response_model=List[TaskDTO])
async def get_my_tasks(
current_user: User | None = Depends(get_current_user),
db: DatabaseDep,
) -> list[TaskDTO]:
"""Get all tasks for the authenticated user"""
if not current_user:
raise HTTPException(status_code=401, detail="Not authenticated")
result = await db.exec(
select(Task).where(Task.user_id == current_user.id)
)
tasks = result.all()
return [TaskDTO.model_validate(task) for task in tasks]
@router.post("/my-tasks", response_model=TaskDTO)
async def create_my_task(
task_data: CreateTaskDTO,
current_user: User | None = Depends(get_current_user),
db: DatabaseDep,
) -> TaskDTO:
"""Create a new task for the authenticated user"""
if not current_user:
raise HTTPException(status_code=401, detail="Not authenticated")
task = Task.model_validate(task_data)
task.user_id = current_user.id
db.add(task)
await db.commit()
await db.refresh(task)
return TaskDTO.model_validate(task)
@router.patch("/{task_id}/toggle", response_model=TaskDTO)
async def toggle_task_completion(
task_id: UUID,
current_user: User | None = Depends(get_current_user),
db: DatabaseDep,
) -> TaskDTO:
"""Toggle the completion status of a task"""
if not current_user:
raise HTTPException(status_code=401, detail="Not authenticated")
result = await db.exec(
select(Task).where(
Task.id == task_id,
Task.user_id == current_user.id
)
)
task = result.first()
if not task:
raise HTTPException(status_code=404, detail="Task not found")
task.completed = not task.completed
await db.commit()
await db.refresh(task)
return TaskDTO.model_validate(task)
# Setup the CRUD routes
_task_crud.setup_routes()
Step 5: Register the Router
Add the router to back/src/main.py:
# Add this import with the other router imports
from routers import task
# Add this line with the other router inclusions
app.include_router(task.router)
Step 6: Test Your API
Start the backend server:
# From the project root
bash scripts/start_all.sh
Navigate to http://localhost:8000/docs to see your new API endpoints in the interactive documentation.
Test the Endpoints
-
Create a task: POST to
/tasks/my-tasks{
"title": "Learn FastAPI",
"description": "Complete the task management tutorial"
} -
Get your tasks: GET
/tasks/my-tasks -
Toggle completion: PATCH
/tasks/{task_id}/toggle
Step 7: Verify Everything Works
Test your complete feature:
# Check database contains your task
cd back
uv run python -c "
from sqlmodel import create_engine, Session, select
from models.db_models import Task
from services.config_service import ConfigService
config = ConfigService()
engine = create_engine(config.get_database_url())
with Session(engine) as session:
tasks = session.exec(select(Task)).all()
print(f'Found {len(tasks)} tasks')
for task in tasks:
print(f'- {task.title}: {\"✓\" if task.completed else \"○\"}')
"
What You've Learned
✅ Database Models: Created SQLModel classes with relationships
✅ API Models: Created DTOs for request/response validation
✅ Database Migrations: Generated and applied schema changes
✅ API Routing: Created both CRUD and custom endpoints
✅ Authentication: Protected routes with user context
🚀 Next Steps
Your task management API is complete! Now build the frontend interface that connects to it:
Frontend: Your First Component →
Learn how to create a React interface that uses your new API with type-safe calls, state management, and a polished UI.