DEV Community

Derek
Derek

Posted on • Originally published at thedevgrind.hashnode.dev

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

Reading Time: ~8 min read


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 as part of my developer roadmap 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Run it:

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

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.

Top comments (0)