HTML Code
HTML Code
Shrink
SQL DDL
HTTP Request
Commands Comm
Business Validation
Logic Layer PostgreSQL 16
Port 5432
Pydantic
- Route Handlers
- Session provider Schemas
(get_db)
Calls ORM
Objectives
Set up PostgreSQL database using Docker Compose
4/14/26, 10:26 AM 2/49 [Link]
Connect FastAPI to PostgreSQL using SQLAlchemy ORM
Define database models (tables) as Python classes
Create Pydantic schemas for request/response validation
Implement full CRUD API for user management
Use Alembic to manage database schema migrations
Test API endpoints with multiple methods
Understand database sessions and connection management
Background
Why Do We Need Databases?
In Lab 1, our API returned data directly from the code. But what happens when:
What is PostgreSQL?
PostgreSQL (often called "Postgres") is one of the world's most advanced open-source relational databases.
users table:
┌────┬──────────────────────┬─────────────┐
│ id │ email │ username │
├────┼──────────────────────┼─────────────┤
│ 1 │ alice@[Link] │ alice │
│ 2 │ bob@[Link] │ bob │
│ 3 │ charlie@[Link] │ charlie │
└────┴──────────────────────┴─────────────┘
SQLAlchemy Components:
Application Layer
Parent Class
Engine
Column Definitions Constraints
(create_engine)
(id, email, username) (unique, nullable, index)
Manages
Spawns Connection Pool DB-API Driver
(Reuses Connections) Reuses (psycopg2)
populates
Session Instance
Connection Layer
4/14/26, 10:26 AM 5/49 [Link] Transmits via
users Table Database PostgreSQL Server PostgreSQL Protocol
Maps to
1. Declarative Base:
2. Engine:
3. SessionLocal:
4. Session:
5. Connection Pool:
6. Models:
Connection
4/14/26, 10:26 AM Pool Management
6/49 [Link]
Connection Lifecycle
Reuse Existing
Connection
Yes
No, At Max
Wait for Available
or Timeout
Application Requests
FastAPI Dependency
get_db Dependency
Session Factory
PostgreSQL Database
PostgreSQL Server
Active Connections: 5
Max Connections: 100
A connection pool is a cache of database connections maintained by SQLAlchemy's Engine. Instead of creating a new connection for every database request (which is slow
and expensive), SQLAlchemy reuses existing connections from the pool.
1. Application Starts
Connection States:
engine = create_engine(
DATABASE_URL,
pool_size=20, # Keep 20 connections always ready
max_overflow=40, # Allow 40 more during spikes (total: 60)
pool_timeout=30, # Wait 30s for connection before error
pool_recycle=3600, # Recycle connections every hour
pool_pre_ping=True, # Test connection before using (catch stale connections)
echo=False
)
The connection pool is invisible to your route handlers but provides massive performance benefits automatically!
4/14/26, 10:26 AM 10/49 [Link]
What are Database Migrations (Alembic)?
The Problem: Your database schema changes over time:
alembic revision
Developer Modifies Models Alembic Detects Changes Create Migrat
--autogenerate
app/[Link] Compares [Link] vs DB alembic/version
-m 'description'
Rollback Process
CRUD Operations
CRUD is an acronym for the four basic database operations:
Docker packages applications in "containers" - isolated environments that run consistently everywhere.
Project Structure
Lab-2/
├── [Link] # PostgreSQL container configuration
├── .env # Environment variables (database URL, app name)
├── .[Link] # Template for environment variables
├── [Link] # Python dependencies
├── [Link] # Alembic configuration file
├── .gitignore # Files to exclude from version control
│
├── app/ # Main application package
│ ├── __init__.py # Makes 'app' a Python package
│ ├── [Link] # FastAPI app + CRUD route handlers
│ ├── [Link] # Database engine, session, and Base class
│ ├── [Link] # SQLAlchemy models (database tables)
│ └── [Link] # Pydantic schemas (request/response validation)
│
└── alembic/ # Database migrations
├── [Link] # Alembic environment configuration
├── [Link] # Template for new migrations
├── README # Alembic documentation
4/14/26, 10:26 AM 14/49 [Link]
└── versions/ # Migration version files
└── xxxx_create_users_table.py # First migration (created by you)
Architecture Components:
1. Client Layer
Web browsers, API testing tools (cURL, Postman), and Python scripts
Send HTTP requests with JSON payloads
3. Database Layer
5. Configuration
1. Client sends: POST /users with JSON {"email": "ada@[Link]", "username": "ada"}
2. FastAPI receives request and validates JSON using Pydantic schema
3. FastAPI calls the create_user function with validated data
4. Function creates SQLAlchemy User object
5. SQLAlchemy translates to SQL: INSERT INTO users ...
6. PostgreSQL executes SQL and returns new user ID
7. SQLAlchemy creates Python User object with ID
4/14/26, 10:26 AM 15/49 [Link]
8. FastAPI serializes object to JSON using Pydantic schema
9. Client receives: {"id": 1, "email": "ada@[Link]", "username": "ada"}
mkdir app
code/
└── app/
fastapi==0.115.5
uvicorn[standard]==0.32.0
python-dotenv==1.0.1
SQLAlchemy==2.0.23
psycopg2-binary==2.9.10
pydantic==2.9.2
alembic==1.13.2
pydantic[email]
APP_NAME=FastAPI Lab 2
DATABASE_URL=postgresql+psycopg2://postgres:postgres@localhost:5432/lab2_db
Explanation:
version: "3.9"
services:
db:
image: postgres:16
container_name: lab2_postgres
environment:
POSTGRES_USER: postgres
4/14/26, 10:26POSTGRES_PASSWORD:
AM 17/49 [Link]
postgres
POSTGRES_DB: lab2_db
ports:
- "5432:5432"
volumes:
- pgdata_lab2:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d lab2_db"]
interval: 5s
timeout: 5s
retries: 20
volumes:
pgdata_lab2:
# Activate it (Linux/Mac)
source .venv/bin/activate
touch app/__init__.py
import os
from dotenv import load_dotenv
from sqlalchemy import create_engine
from [Link] import sessionmaker, DeclarativeBase
Key Concepts:
touch app/[Link]
class User(Base):
__tablename__ = "users"
# Email column
4/14/26, 10:26 AM 21/49 [Link]
email = Column(
String(255), # Maximum 255 characters
nullable=False, # Cannot be NULL
unique=True, # Must be unique across all users
index=True # Create index for faster lookups
)
# Username column
username = Column(
String(50), # Maximum 50 characters
nullable=False, # Cannot be NULL
unique=True, # Must be unique across all users
index=True # Create index for faster lookups
)
# Table-level constraints
__table_args__ = (
UniqueConstraint("email", name="uq_users_email"),
UniqueConstraint("username", name="uq_users_username"),
)
def __repr__(self):
"""String representation of User object."""
return f"<User(id={[Link]}, email='{[Link]}', username='{[Link]}')>"
touch app/[Link]
class UserCreate(BaseModel):
email: EmailStr # Validates email format
username: str = Field(
min_length=3, # Minimum 3 characters
max_length=50 # Maximum 50 characters
)
class UserUpdate(BaseModel):
email: EmailStr | None = None # Optional email
username: str | None = Field(
default=None,
min_length=3,
max_length=50
)
class Config:
"""Pydantic configuration."""
from_attributes = True # Allow creating from ORM models
Benefits:
Automatic validation
Clear API documentation
Type safety
Prevents sending unwanted data
touch app/[Link]
import os
from dotenv import load_dotenv
from fastapi import FastAPI, Depends, HTTPException, status
from [Link] import Session
if existing_user:
raise HTTPException(
status_code=400,
detail="Email or username already exists"
)
return user
if not user:
raise HTTPException(
4/14/26, 10:26 AM 26/49 [Link]
status_code=404,
detail="User not found"
)
return user
if not user:
raise HTTPException(
status_code=404,
detail="User not found"
)
return user
if not user:
raise HTTPException(
status_code=404,
detail="User not found"
)
Key Patterns:
This creates:
alembic/
├── [Link] # Environment configuration
├── README # Alembic documentation
├── [Link] # Template for new migrations
└── versions/ # Migration files go here
target_metadata = None
Also, find the run_migrations_offline() and run_migrations_online() functions and ensure they read from your .env file. Add this at the top of [Link] after imports:
[Link] = postgresql+psycopg2://postgres:postgres@localhost:5432/lab2_db
Command breakdown:
4/14/26, 10:26 AM 33/49 [Link]
[Link]: Import app from app/[Link]
app: The FastAPI instance
--reload: Auto-restart on code changes
Find the wt0 IP address for the Poridhi's VM currently you are running by using the command:
ifconfig
{
"email": "alice@[Link]",
"username": "alice"
}
5. Click "Execute"
6. See response with status 201 and the created user with ID
Create test_users.py:
import requests
BASE_URL = "[Link]
# UPDATE user
response = [Link](
f"{BASE_URL}/users/{user_id}",
json={"email": "[Link]@[Link]"}
)
print(f"\nUPDATE: {response.status_code}")
print([Link]())
# DELETE user
response = [Link](f"{BASE_URL}/users/{user_id}")
print(f"\nDELETE: {response.status_code}")
Run it:
4/14/26, 10:26 AM 43/49 [Link]
pip install requests
python test_users.py
# Connect to database
docker compose exec db psql -U postgres -d lab2_db
# Count users
SELECT COUNT(*) FROM users;
# Exit psql
\q
Now let's demonstrate a real-world scenario: adding new columns to an existing table. This shows how Alembic helps you evolve your database schema over time.
Let's add created_at and is_active columns to track when users were created and whether they're active.
18.1: Update
4/14/26, 10:26 AMthe User
44/49 Model
[Link]
Edit app/[Link]:
class User(Base):
__tablename__ = "users"
# New columns
created_at = Column(DateTime(timezone=True), server_default=[Link](), nullable=False)
is_active = Column(Boolean, default=True, nullable=False)
What changed:
Expected output:
4/14/26, 10:26 AM 45/49 [Link]
18.3: Review Generated Migration
Expected output:
Expected output:
docker compose exec db psql -U postgres -d lab2_db -c "SELECT id, username, created_at, is_active FROM users;"
You should see existing users now have created_at set to migration time and is_active = true.
This second migration demonstrates the real power of Alembic. In production applications, your database schema constantly evolves:
Real-World Impact: Imagine you have 10,000 users in production and need to add a last_login column. With Alembic:
Without migrations, you'd manually write SQL, risk inconsistencies between environments, and potentially lose data.
Key Takeaway: Alembic migrations are version control for your database schema. Just like Git tracks code changes, Alembic tracks schema changes, enabling safe
evolution across development, staging, and production environments.
Conclusion
Congratulations on completing Lab 2! You've built a production-ready CRUD API with FastAPI and PostgreSQL, learned how SQLAlchemy manages database connections
efficiently through connection pooling, and mastered Alembic migrations for evolving your database schema safely. These fundamentals form the backbone of modern web
applications—from handling thousands of concurrent users to deploying schema changes in production without downtime. You're now equipped to build robust, scalable
APIs that connect to real databases.