Creating a CRUD app with FastAPI + Postgresql

π§ PostgreSQL Setup
β 1. Install PostgreSQL (if not already)
sudo apt update
sudo apt install postgresql postgresql-contrib
β 2. Start PostgreSQL service
sudo service postgresql start
π§βπ» 3. Create a PostgreSQL User & Database
Step A: Open PostgreSQL shell
sudo -u postgres psql
Step B: Create a user
CREATE USER raj WITH PASSWORD 'yourpassword';
Step C: Create a database
CREATE DATABASE fastapidb OWNER raj;
Step D: Give privileges
GRANT ALL PRIVILEGES ON DATABASE fastapidb TO raj;
Step E: Exit psql
\q
π 4. Database URL Format :
postgresql://raj:yourpassword@localhost/fastapidb
π§ FastAPI Learning Roadmap
π Step 1: Introduction & Setup
β What is FastAPI?
β Installing FastAPI + Uvicorn
β Running your first API
π Step 2: Core Concepts
π₯ Path & query parameters (
@app.get
)π§Ύ Request bodies (
@
app.post
)π§ Pydantic models (validation)
π€ Response models
π Step 3: Intermediate Usage
- π§© CRUD with database (SQLAlchemy + PostgreSQL β already covered)
π§ FastAPI - Deep Introduction
π¦ Installing FastAPI
Step 1: Create a virtual environment
python3 -m venv venv
source venv/bin/activate
Step 2: Install FastAPI & Uvicorn
pip install fastapi[all]
fastapi
β the actual framework[all]
installs optional dependencies likepydantic
,uvicorn
, and doc supportuvicorn
β lightweight ASGI server to run FastAPI apps
π Create your first FastAPI app
π File: main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello Raj! Welcome to FastAPI!"}
βΆοΈ Run the app
In terminal:
uvicorn main:app --reload
Here:
main
= filename (without.py
)app
= FastAPI instance name inside the file--reload
= auto-reload on file change (great during development)π Visit these URLs in browser:
URL | Purpose |
http://127.0.0.1:8000/ | Base route β shows hello message |
http://127.0.0.1:8000/docs | Swagger UI (test all APIs) |
πΉ @app.get()
with Path & Query Parameters (Deep Dive)
β 1. Path Parameters
π Use-case:
When you want to pass a required value in the URL itself, like /items/5
, where 5
is the item_id
.
π§ Code Example:
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def get_item(item_id: int):
return {"item_id": item_id}
π§ Explanation:
/items/{item_id}
means FastAPI is expecting a value at that URL location.item_id: int
automatically:Converts the path string to an integer
Throws an error if the value is not an integer
π‘ Try:
Visit this in your browser:
http://127.0.0.1:8000/items/101
π Output:
{ "item_id": 101 }
β If you visit /items/abc
(non-integer):
You'll get:
{
"detail": [
{
"loc": ["path", "item_id"],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
This is auto-generated validation from Pydantic + FastAPI π€― β no manual if-else needed!
β 2. Query Parameters
π Use-case:
Optional or filter-based values passed after ?
in a URL. Like:
/products?skip=10&limit=5
π§ Code Example:
@app.get("/products")
def list_products(skip: int = 0, limit: int = 10):
return {"skip": skip, "limit": limit}
π‘ Try:
Visit:
http://127.0.0.1:8000/products?skip=5&limit=2
π Output:
{ "skip": 5, "limit": 2 }
β What's Happening Behind the Scenes?
FastAPI is:
Reading query strings (
?skip=5&limit=2
)Matching them with function parameters
Automatically validating types (
int
)Using default values if theyβre not provided
π§ Bonus: Combine Path + Query
@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
return {"item_id": item_id, "q": q}
URL:
http://127.0.0.1:8000/items/5?q=hello
Result:
{
"item_id": 5,
"q": "hello"
}
π§Ύ Step 3: Request Body with Pydantic Models (POST requests)
π What is a Request Body?
When a client sends data inside the body of a request β not in the URL β for example when creating a new user, item, product, etc.
This usually happens with POST or PUT methods.
π¦ Pydantic: The Power Engine
FastAPI uses Pydantic models (Python classes) to:
β Automatically validate incoming JSON
β Convert it to Python objects
β Auto-generate Swagger docs
β Step-by-Step Example
π main.py
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# Step 1: Define a model for the data you expect
class Item(BaseModel):
name: str
description: str
price: float
in_stock: bool = True # default value
# Step 2: Accept that model as a parameter in your POST endpoint
@app.post("/items/")
def create_item(item: Item):
return {
"name": item.name,
"desc": item.description,
"price": item.price,
"available": item.in_stock
}
βΆοΈ Run and test:
uvicorn main:app --reload
Visit:
http://127.0.0.1:8000/docs
Click on POST /items/
β Try it out β Paste:
{
"name": "Mouse",
"description": "Wireless Logitech mouse",
"price": 499.99
}
π Response:
{
"name": "Mouse",
"desc": "Wireless Logitech mouse",
"price": 499.99,
"available": true
}
π― Goal of Response Models in FastAPI
Even though your function might return a lot of data (or even sensitive data), you can use response models to:
β Return only the fields you want
β Validate and shape the output
β Auto-generate clean, accurate docs
β Example Use Case
Suppose we store passwords or other sensitive fields in the DB β we should never send those back in API responses.
So we define 2 models:
π§Ύ
ItemCreate
β for request body (input)π€
ItemResponse
β for response model (output)
π§ͺ Step-by-Step Example
π main.py
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# Input model (request)
class ItemCreate(BaseModel):
name: str
description: str
price: float
secret_code: str # π to be hidden in response
# Output model (response)
class ItemResponse(BaseModel):
name: str
description: str
price: float
@app.post("/items/", response_model=ItemResponse)
def create_item(item: ItemCreate):
# Normally you would save to DB here...
return item # Will be filtered by response_model
β
Try it in /docs
POST /items/ with:
{
"name": "Keyboard",
"description": "Mechanical RGB keyboard",
"price": 2500.0,
"secret_code": "12345"
}
π’ Response:
{
"name": "Keyboard",
"description": "Mechanical RGB keyboard",
"price": 2500.0
}
β
secret_code
is not returned, even though the function returned it β because response_model=ItemResponse
filtered it out!
π§ Behind the Scenes
Concept | What it does |
BaseModel | Defines schema for input or output |
response_model | Tells FastAPI to filter the response by this model |
Returns data | Pydantic filters, formats, and validates |
π Use Case in Real Projects
If you have a User
model with password
field:
class UserIn(BaseModel):
username: str
password: str # don't return this
class UserOut(BaseModel):
username: str
@app.post("/register", response_model=UserOut)
def register_user(user: UserIn):
# hash password and save to DB
return user # password will be filtered from response
Letβs now build the full CRUD project with FastAPI + PostgreSQL using the modular structure β one file at a time, with clear walkthroughs.
π Project Structure Overview
fastapi_postgres_crud/
βββ main.py
βββ database.py
βββ models.py
βββ schemas.py
βββ crud.py
βββ routers/
βββ items.py
β
Step 1: database.py
β DB Connection Setup
π File: database.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, declarative_base
# π§ Update your own credentials here
DATABASE_URL = "postgresql://raj:yourpassword@localhost/yourdb"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)
Base = declarative_base()
β
Your Task:
Replace "yourpassword"
and "yourdb"
with your actual PostgreSQL credentials.
π‘ What this does:
Connects to your PostgreSQL DB
Prepares
SessionLocal
for queryingCreates a
Base
class to define models
Great! Now letβs move on to the next file: defining our table structure using SQLAlchemy ORM.
β
Step 2: models.py
β Define DB Table (SQLAlchemy Model)
π File: models.py
from sqlalchemy import Column, Integer, String, Float, Boolean
from database import Base
class Item(Base):
__tablename__ = "items"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False)
description = Column(String)
price = Column(Float, nullable=False)
in_stock = Column(Boolean, default=True)
π§ Explanation:
Item
is our DB model mapped to theitems
table.Each class attribute is a column in the table.
Base
is inherited fromdatabase.py
to register the model.nullable=False
ensuresname
andprice
are required.
π Tip: This table will only be created when we run this in main.py
β weβll get there soon:
models.Base.metadata.create_all(bind=engine)
β
Step 3: schemas.py
β Define Input/Output Pydantic Models
π File: schemas.py
from pydantic import BaseModel
# Input model (for creating a new item)
class ItemCreate(BaseModel):
name: str
description: str
price: float
in_stock: bool = True
# Output model (for response)
class ItemResponse(BaseModel):
id: int
name: str
description: str
price: float
in_stock: bool
class Config:
from_attributes = True # allows SQLAlchemy to Pydantic conversion
π§ Explanation:
ItemCreate
:What the client must send in the body (e.g.,
POST /items
)Doesnβt include
id
because thatβs auto-generated
ItemResponse
:Sent back to the client (e.g.,
GET /items
)Includes
id
because itβs created by the DB
from_attributes = True
:- Tells Pydantic to accept ORM objects (like SQLAlchemy models)
β
Once you save this file, let me know and weβll proceed to the heart of data handling: crud.py
β where we write functions to interact with the database.
β
Step 4: crud.py
β Database Logic (Create + Read)
π File: crud.py
from sqlalchemy.orm import Session
import models
import schemas
# Create a new item
def create_item(db: Session, item: schemas.ItemCreate):
db_item = models.Item(**item.dict()) # Convert Pydantic to SQLAlchemy
db.add(db_item)
db.commit()
db.refresh(db_item) # Get updated DB record (with ID)
return db_item
# Get all items
def get_items(db: Session):
return db.query(models.Item).all()
# Get single item by ID
def get_item_by_id(db: Session, item_id: int):
return db.query(models.Item).filter(models.Item.id == item_id).first()
# Delete item
def delete_item(db: Session, item_id: int):
db_item = db.query(models.Item).filter(models.Item.id == item_id).first()
if db_item:
db.delete(db_item)
db.commit()
return db_item
# Update item
def update_item(db: Session, item_id: int, updated_item: schemas.ItemCreate):
db_item = db.query(models.Item).filter(models.Item.id == item_id).first()
if db_item:
for key, value in updated_item.dict().items():
setattr(db_item, key, value)
db.commit()
db.refresh(db_item)
return db_item
π This is not FastAPI code yet β this is just database access. The routes will call these functions.
β
Let me know when this is saved and Iβll move to step 5: routers/
items.py
, where we finally expose these as actual API endpoints!
β
Step 5: routers/
items.py
β Define Routes (API Endpoints)
π File: routers/
items.py
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from database import SessionLocal
import schemas, crud
from typing import List
router = APIRouter(
prefix="/items",
tags=["Items"]
)
# Dependency to get DB session per request
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@router.post("/", response_model=schemas.ItemResponse)
def create(item: schemas.ItemCreate, db: Session = Depends(get_db)):
return crud.create_item(db, item)
@router.get("/", response_model=List[schemas.ItemResponse])
def read_all(db: Session = Depends(get_db)):
return crud.get_items(db)
@router.get("/{item_id}", response_model=schemas.ItemResponse)
def read_one(item_id: int, db: Session = Depends(get_db)):
db_item = crud.get_item_by_id(db, item_id)
if not db_item:
raise HTTPException(status_code=404, detail="Item not found")
return db_item
@router.put("/{item_id}", response_model=schemas.ItemResponse)
def update(item_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db)):
updated = crud.update_item(db, item_id, item)
if not updated:
raise HTTPException(status_code=404, detail="Item not found")
return updated
@router.delete("/{item_id}", response_model=schemas.ItemResponse)
def delete(item_id: int, db: Session = Depends(get_db)):
deleted = crud.delete_item(db, item_id)
if not deleted:
raise HTTPException(status_code=404, detail="Item not found")
return deleted
Perfect! Letβs now wrap it up with the final piece: main.py
, where we launch the FastAPI app and connect everything together.
β
Step 6: main.py
β App Entry Point
π File: main.py
from fastapi import FastAPI
import models
from database import engine
from routers import items
# Create all tables (runs once on startup)
models.Base.metadata.create_all(bind=engine)
# Initialize app
app = FastAPI()
# Register the items router
app.include_router(items.router)
π§ Whatβs Happening Here:
Line | Purpose |
models.Base.metadata.create_all() | Creates tables in DB (if not exists) |
FastAPI() | Initializes the API app |
app.include_router(items.router) | Registers routes from routers/items.py |
β Launch the App
Go to your project root and run:
uvicorn main:app --reload
Then open: http://127.0.0.1:8000/docs
π Youβll see your full CRUD API in Swagger!
π Final Checklist Recap
File | Purpose |
database.py | DB connection + session |
models.py | Table schema (SQLAlchemy) |
schemas.py | Input/Output Pydantic models |
crud.py | DB interaction logic |
routers/ items.py | API endpoints |
main.py | App bootstrap + router connect |
Subscribe to my newsletter
Read articles from Rajesh Gurajala directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
