DEV Community

Muha-mmed
Muha-mmed

Posted on

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? 😊

Top comments (0)