DEV Community

Muha-mmed
Muha-mmed

Posted on

1 1 1 1 1

Building a Production-Ready To-Do API with FastAPI

Hey fellow Python devs! πŸ‘‹

In this guide, we’ll build a To-Do API using FastAPI with production-ready features. If you're looking to sharpen your backend development skills or add a solid project to your portfolio, this tutorial is for you!

You'll learn how to:

βœ… Set up a PostgreSQL database

βœ… Define models with SQLAlchemy

βœ… Validate data using Pydantic

βœ… Implement CRUD operations with FastAPI

Let’s get started! πŸš€


Project Setup

Step 1: Install Dependencies

Before we start coding, install the required dependencies:

pip install fastapi psycopg2 pydantic SQLAlchemy uvicorn python-dotenv
Enter fullscreen mode Exit fullscreen mode

Step 2: Project Structure

Your project should be structured like this:

πŸ“‚ todo_api_project
 ┣ πŸ“œ main.py        # Entry point of the application
 ┣ πŸ“œ database.py    # Database configuration
 ┣ πŸ“œ model.py       # Database models
 ┣ πŸ“œ schema.py      # Pydantic schemas
 ┣ πŸ“œ route.py       # API routes
 β”— πŸ“œ requirements.txt # Dependencies
Enter fullscreen mode Exit fullscreen mode

Step 3: Setting Up the Database

We’ll use PostgreSQL as our database. Create a file called database.py and add the following code:

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "postgresql://yourusername:yourpassword@localhost/todo_db"

engine = create_engine(DATABASE_URL)

sessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

BASE = declarative_base()

def get_db():
    db = sessionLocal()
    try:
        yield db
    finally:
        db.close()
Enter fullscreen mode Exit fullscreen mode

Explanation

  • We use create_engine to connect to the PostgreSQL database.
  • sessionLocal manages database sessions.
  • BASE = declarative_base() allows us to define database models.
  • The get_db function provides a database session when needed.

Step 4: Creating the To-Do Model

In model.py, define the structure of your To-Do items using SQLAlchemy:

from sqlalchemy import Column, Integer, String, Boolean, TIMESTAMP, text
from database import BASE

class Todo(BASE):
    __tablename__ = "todos"

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, nullable=False)
    description = Column(String, nullable=True)
    published = Column(Boolean, server_default=text("False"))
    created_at = Column(TIMESTAMP(timezone=True), server_default=text("now()"))
Enter fullscreen mode Exit fullscreen mode

Explanation

  • id: Auto-generated primary key.
  • title: Required (nullable=False).
  • description: Optional field (nullable=True).
  • published: Boolean field with a default value of False.
  • created_at: Stores the creation timestamp.

Step 5: Creating Pydantic Schemas

In schema.py, define how Pydantic will validate incoming request data:

from typing import Optional
from pydantic import BaseModel

class Todo(BaseModel):
    title: str
    description: Optional[str] = None
    published: bool = False

    class Config:
        orm_mode = True

class UpdateTodo(BaseModel):
    title: Optional[str] = None
    description: Optional[str] = None
    published: Optional[bool] = None
Enter fullscreen mode Exit fullscreen mode

Explanation

  • Todo: Used for creating new tasks.
  • UpdateTodo: Used for updating tasks.
  • orm_mode = True: Ensures compatibility with SQLAlchemy models.

Step 6: Creating API Routes

Now, let’s create the API endpoints in route.py:

from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from database import get_db, engine
from schema import Todo, UpdateTodo
import model
from typing import List

# Create tables
model.BASE.metadata.create_all(bind=engine)

route = APIRouter()
Enter fullscreen mode Exit fullscreen mode

1️⃣ Get All To-Dos

@route.get("/getalltodos")
def get_all_todo(db: Session = Depends(get_db)):
    todos = db.query(model.Todo).all()
    return todos if todos else []
Enter fullscreen mode Exit fullscreen mode

βœ… Fetches all tasks from the database.


2️⃣ Create a New To-Do

@route.post("/todo/create")
def create_todo(todo: Todo, db: Session = Depends(get_db)):
    todo_item = model.Todo(**todo.model_dump())
    db.add(todo_item)
    db.commit()
    db.refresh(todo_item)
    return todo_item
Enter fullscreen mode Exit fullscreen mode

βœ… Takes JSON input and adds a new task to the database.


3️⃣ Update a To-Do

@route.put("/todo/update/{post_id}", response_model=UpdateTodo)
def update_todo(new_post: UpdateTodo, post_id: int, db: Session = Depends(get_db)):
    todo = db.query(model.Todo).filter(model.Todo.id == post_id).first()
    if not todo:
        return {"error": "To-do not found"}

    update_data = new_post.model_dump(exclude_unset=True)

    for key, value in update_data.items():
        setattr(todo, key, value)

    db.commit()
    db.refresh(todo)
    return todo
Enter fullscreen mode Exit fullscreen mode

βœ… Updates an existing task based on post_id.


4️⃣ Get Published To-Dos

@route.get("/published_todo")
def get_published_todos(db: Session = Depends(get_db)):
    return db.query(model.Todo).filter(model.Todo.published == True).all()
Enter fullscreen mode Exit fullscreen mode

βœ… Fetches only published to-do items.


5️⃣ Get Draft To-Dos

@route.get("/draft_todo")
def get_draft_post(db: Session = Depends(get_db)):
    return db.query(model.Todo).filter(model.Todo.published == False).all()
Enter fullscreen mode Exit fullscreen mode

βœ… Fetches only unpublished (draft) tasks.


6️⃣ Get a To-Do by ID

@route.get("/getbyid/{todo_id}")
def get_by_id(todo_id: int, db: Session = Depends(get_db)):
    return db.query(model.Todo).filter(model.Todo.id == todo_id).first()
Enter fullscreen mode Exit fullscreen mode

βœ… Fetches a specific task by its id.


7️⃣ Search To-Dos

@route.get("/search", response_model=List[Todo])
def search_todo(name: str = Query(None), db: Session = Depends(get_db)):
    query = db.query(model.Todo)

    if name:
        query = query.filter(model.Todo.title.like(f"%{name}%") | model.Todo.description.like(f"%{name}%"))
        todos = query.all()
        return todos if todos else {"error": "No post found"}

    return []
Enter fullscreen mode Exit fullscreen mode

βœ… Searches tasks by title or description.


8️⃣ Delete a To-Do

@route.delete("/delete/{todo_id}")
def delete_post(todo_id: int, db: Session = Depends(get_db)):
    todo = db.query(model.Todo).filter(model.Todo.id == todo_id).first()
    db.delete(todo)
    db.commit()
Enter fullscreen mode Exit fullscreen mode

βœ… Deletes a task from the database.


Step 7: Running the API

Create main.py to start the server:

from fastapi import FastAPI
from route import route  

app = FastAPI()

@app.get("/")
def root():
    return {"hello": "world!!!"}

app.include_router(route)
Enter fullscreen mode Exit fullscreen mode

Run the server:

uvicorn main:app --reload
Enter fullscreen mode Exit fullscreen mode

Visit http://127.0.0.1:8000/docs to test the API! πŸŽ‰


Conclusion

You've built a To-Do API with FastAPI, PostgreSQL, and SQLAlchemy! This is a great project to showcase in your portfolio. Keep exploring and refining your skills! πŸš€

Full code on my GitHub here

What do you think? Does this structure work for your blog? 😊

API Trace View

Struggling with slow API calls?

Dan Mindru walks through how he used Sentry's new Trace View feature to shave off 22.3 seconds from an API call.

Get a practical walkthrough of how to identify bottlenecks, split tasks into multiple parallel tasks, identify slow AI model calls, and more.

Read more β†’

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

πŸ‘‹ Kindness is contagious

Please leave a ❀️ or a friendly comment on this post if you found it helpful!

Okay