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…

How to Build a FastAPI Todo App Backend (with SQLite) — Step-by-Step Tutorial
Photo by Sergei A on Unsplash

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 for POST requests (create a new todo).
TodoUpdate: Schema for PUT 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.

Photo by Priscilla Du Preez 🇨🇦 on Unsplash