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
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, androuters/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)