Skip to main content

Command Palette

Search for a command to run...

How I Built a Rental Listings REST API with FastAPI and PostgreSQL

Updated
•5 min read
How I Built a Rental Listings REST API with FastAPI and PostgreSQL
D
SE student in Kenya šŸ‡°šŸ‡Ŗ building with Python, FastAPI & cybersecurity. Documenting the grind. Building Kagua — a civic tech platform for Kenya. Follow along if you're learning too.

Introduction

Ever wondered what actually goes into building a real backend API, not a toy example, but something with a database, real endpoints, and proper structure?

I did too. So I built one.

This is a breakdown of how I built a Rental Listings REST API using FastAPI and PostgreSQL and what I learned along the way.


Prerequisites

  • Basic Python knowledge
  • Python 3.10+ installed
  • PostgreSQL installed and running
  • Familiarity with the terminal

What You'll Learn

  • How to set up a FastAPI project from scratch
  • How to define database models with SQLAlchemy
  • How to connect FastAPI to PostgreSQL
  • How to build full CRUD endpoints
  • How to test your API using Swagger UI

Why FastAPI?

Think of FastAPI like a very efficient waiter at a restaurant.

You place an order (send a request), the waiter takes it to the kitchen (your logic), and brings back exactly what you asked for, fast, no confusion.

FastAPI handles all the routing, validation, and documentation automatically. You focus on the logic.

pip install fastapi uvicorn sqlalchemy psycopg2-binary

Project Structure

Before writing a single line of code, I organized the project cleanly:

rental_api/
ā”œā”€ā”€ main.py
ā”œā”€ā”€ database.py
ā”œā”€ā”€ models.py
ā”œā”€ā”€ schemas.py
└── routers/
    └── listings.py

Clean structure equals easier debugging later. Trust me on this one.


Step 1 — Connecting to PostgreSQL

This is where SQLAlchemy comes in. Think of SQLAlchemy as a translator between your Python code and the PostgreSQL database. You write Python, it speaks SQL.

# database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

DATABASE_URL = "postgresql://user:password@localhost/rental_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()

The get_db function gives each request its own database session and closes it cleanly when done.


Step 2 — Defining the Database Model

# models.py
from sqlalchemy import Column, Integer, String, Float
from database import Base

class Listing(Base):
    __tablename__ = "listings"

    id = Column(Integer, primary_key=True, index=True)
    title = Column(String, nullable=False)
    location = Column(String, nullable=False)
    price = Column(Float, nullable=False)
    bedrooms = Column(Integer, nullable=False)

This is the blueprint for my database table. SQLAlchemy creates the actual table in PostgreSQL automatically when the app starts.


Step 3 — Setting Up Pydantic Schemas

Schemas control what data comes into the API and what goes out.

# schemas.py
from pydantic import BaseModel

class ListingBase(BaseModel):
    title: str
    location: str
    price: float
    bedrooms: int

class ListingCreate(ListingBase):
    pass

class ListingResponse(ListingBase):
    id: int

    class Config:
        orm_mode = True

The orm_mode = True line is critical. Without it FastAPI cannot serialize SQLAlchemy objects into JSON responses.


Step 4 — Building the CRUD Endpoints

# routers/listings.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from database import get_db
from models import Listing
from schemas import ListingCreate, ListingResponse
from typing import List

router = APIRouter(prefix="/listings", tags=["Listings"])

# CREATE
@router.post("/", response_model=ListingResponse)
def create_listing(listing: ListingCreate, db: Session = Depends(get_db)):
    new_listing = Listing(**listing.dict())
    db.add(new_listing)
    db.commit()
    db.refresh(new_listing)
    return new_listing

# READ ALL
@router.get("/", response_model=List[ListingResponse])
def get_listings(db: Session = Depends(get_db)):
    return db.query(Listing).all()

# READ ONE
@router.get("/{id}", response_model=ListingResponse)
def get_listing(id: int, db: Session = Depends(get_db)):
    listing = db.query(Listing).filter(Listing.id == id).first()
    if not listing:
        raise HTTPException(status_code=404, detail="Listing not found")
    return listing

# UPDATE
@router.put("/{id}", response_model=ListingResponse)
def update_listing(id: int, updated: ListingCreate, db: Session = Depends(get_db)):
    listing = db.query(Listing).filter(Listing.id == id).first()
    if not listing:
        raise HTTPException(status_code=404, detail="Listing not found")
    for key, value in updated.dict().items():
        setattr(listing, key, value)
    db.commit()
    db.refresh(listing)
    return listing

# DELETE
@router.delete("/{id}")
def delete_listing(id: int, db: Session = Depends(get_db)):
    listing = db.query(Listing).filter(Listing.id == id).first()
    if not listing:
        raise HTTPException(status_code=404, detail="Listing not found")
    db.delete(listing)
    db.commit()
    return {"message": "Listing deleted successfully"}

Step 5 — Wiring Everything Together

# main.py
from fastapi import FastAPI
from database import engine, Base
from routers import listings

Base.metadata.create_all(bind=engine)

app = FastAPI(title="Rental Listings API")
app.include_router(listings.router)

Run it:

uvicorn main:app --reload

Open http://127.0.0.1:8000/docs and your full API is live and testable in Swagger UI.


Common Mistakes

Forgetting orm_mode = True in your schema → Always add it to your response models

Not closing the DB session → Use get_db() as a dependency, it handles this for you

Returning a 200 on a missing resource → Always raise HTTPException(404) when a record is not found

Hardcoding your database URL → Use environment variables with python-dotenv

Skipping Pydantic validation → Always separate your request schema from your response schema


Best Practices

  • Keep models.py, schemas.py, and routers/ separate, never mix them
  • Always validate input with Pydantic before touching the database
  • Use Depends(get_db) consistently, never create sessions manually inside routes
  • Test every endpoint in Swagger UI before moving on
  • Commit to GitHub after every working feature

Summary

In this post I walked through building a full Rental Listings REST API with FastAPI and PostgreSQL. We covered project structure, connecting to a database with SQLAlchemy, defining models and schemas, and building all five CRUD endpoints.

The biggest lesson? Structure matters before code. A clean project layout saved me hours of confusion.


What is Next?

  • Adding authentication with JWT tokens
  • Deploying the API to Railway

Call to Action

If you are learning backend development, try building this yourself, even if you change the topic from rentals to something you care about.

Follow my blog for weekly posts as I document the full journey.

Got questions? Drop them in the comments.

10 views