How to Build a FastAPI Todo App Backend (with SQLite) — Step-by-Step Tutorial
Want to learn FastAPI and build something useful while doing it? This hands-on guide walks you through creating a real-world Todo API with…

Build Something Real in Under 20 Minutes.
How to Build a FastAPI Todo App Backend (with SQLite) — Step-by-Step Tutorial
Want to learn FastAPI and build something useful while doing it? This hands-on guide walks you through creating a real-world Todo API with SQLite in minutes — perfect for backend beginners and productivity app fans.
If you’re learning FastAPI, it’s easy to get lost in the documentation or jump from tutorial to tutorial without building anything tangible. This guide fixes that.
We’re going to build a real-world, production-ready Todo app backend using FastAPI and SQLite. No fluff. Just code, structure, and the mindset you’ll need to build bigger things.
Whether you’re preparing for your next project, coding interview, or just exploring FastAPI, this guide will help you get hands-on experience with:
- FastAPI endpoints (CRUD)
- SQLite database (via SQLAlchemy)
- Pydantic models
- Dependency injection and modular code
Tech Stack
We’ll use:
FastAPI — High-performance Python web framework
SQLite — Lightweight, file-based SQL database
SQLAlchemy — ORM for database models and queries
Pydantic — Data validation and serialization
Project Structure
Let’s start by organizing the project:
todo_app/
│
├── main.py
├── models.py
├── database.py
└── schemas.py
Set Up Your Environment
Let’s create a virtual environment and install the required packages
mkdir todo_app && cd todo_app
python -m venv venv
source venv/bin/activate # or .\venv\Scripts\activate on Windows
pip install fastapi uvicorn sqlalchemy
Define the Database (database.py
)
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker
DATABASE_URL = "sqlite:///./todos.db"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)
Base = declarative_base()
What’s happening here?
engine
: Connects SQLAlchemy to the SQLite DB.
SessionLocal
: Used to interact with the DB session (insert, query, delete, etc.).
Base
: The base class for all our models (like tables).
Defining the Todo Table (models.py
)
from sqlalchemy import Column, Integer, String, Boolean
from database import Base
class Todo(Base):
__tablename__ = "todos"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
description = Column(String, nullable=True)
completed = Column(Boolean, default=False)
What’s happening here?
We define the Todo
model, which maps directly to the todos table in SQLite.
Each attribute (e.g.,title
,completed
) becomes a column in the table.
Base
lets SQLAlchemy understand that this is a DB model.
Define Schemas with Pydantic (schemas.py
)
from pydantic import BaseModel
class TodoCreate(BaseModel):
title: str
description: str = None
class TodoUpdate(BaseModel):
title: str = None
description: str = None
completed: bool = None
class TodoResponse(BaseModel):
id: int
title: str
description: str = None
completed: bool
class Config:
orm_mode = True
What’s happening here?
TodoCreate
: Schema forPOST
requests (create a new todo).
TodoUpdate
: Schema forPUT
requests (update a todo).
TodoResponse
: Schema for sending todo data back in API responses.
orm_mode = True
: Enables Pydantic to work with SQLAlchemy objects.
Building the FastAPI App (main.py
)
from fastapi import FastAPI, HTTPException, Depends
from sqlalchemy.orm import Session
import models, schemas
from database import SessionLocal, engine
# Create the database tables
models.Base.metadata.create_all(bind=engine)
app = FastAPI(title="Todo App API")
# Dependency
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
Breakdown:
create_all
: Creates tables in SQLite if they don’t already exist.
get_db()
: Dependency that provides a database session to each request.
Add CRUD Endpoints
Create a Todo
Accepts title
and description
, inserts into DB, and returns the new todo.
@app.post("/todos/", response_model=schemas.TodoResponse)
def create_todo(todo: schemas.TodoCreate, db: Session = Depends(get_db)):
db_todo = models.Todo(**todo.dict())
db.add(db_todo)
db.commit()
db.refresh(db_todo)
return db_todo
Get All Todos
Fetches and returns all todos in the database.
@app.get("/todos/", response_model=list[schemas.TodoResponse])
def read_todos(db: Session = Depends(get_db)):
return db.query(models.Todo).all()
Get a Todo by ID
Returns a specific todo by its ID or raises a 404 if it doesn’t exist.
@app.get("/todos/{todo_id}", response_model=schemas.TodoResponse)
def read_todo(todo_id: int, db: Session = Depends(get_db)):
todo = db.query(models.Todo).filter(models.Todo.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="Todo not found")
return todo
Update a Todo
Only updates fields provided in the request using exclude_unset=True
.
@app.put("/todos/{todo_id}", response_model=schemas.TodoResponse)
def update_todo(todo_id: int, updates: schemas.TodoUpdate, db: Session = Depends(get_db)):
todo = db.query(models.Todo).filter(models.Todo.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="Todo not found")
for key, value in updates.dict(exclude_unset=True).items():
setattr(todo, key, value)
db.commit()
db.refresh(todo)
return todo
Delete a Todo
Deletes a todo if found; otherwise returns 404.
@app.delete("/todos/{todo_id}")
def delete_todo(todo_id: int, db: Session = Depends(get_db)):
todo = db.query(models.Todo).filter(models.Todo.id == todo_id).first()
if not todo:
raise HTTPException(status_code=404, detail="Todo not found")
db.delete(todo)
db.commit()
return {"message": "Todo deleted successfully"}
Test Your API
Run the server:
uvicorn main:app --reload
Open your browser and visit:
http://127.0.0.1:8000/docs
You’ll see an interactive Swagger UI to test all your endpoints.
Conclusion: What You’ve Learned
You just built a complete CRUD API using FastAPI and SQLite. In under 100 lines of code, you learned how to:
- Design clean and modular backend apps
- Integrate SQLite with FastAPI via SQLAlchemy
- Use Pydantic for schema validation
- Build real-world API endpoints
This is a great foundation for bigger apps: user authentication, frontend integration, or deploying your API to the cloud.
