FastAPI PostgreSQL Tutorial: Build Production-Ready Python APIs

Learn to build scalable REST APIs with FastAPI and PostgreSQL. Complete guide covering authentication, testing, and production deployment strategies.

FastAPI PostgreSQL Tutorial: Build Production-Ready Python APIs

Building robust, scalable REST APIs is a cornerstone of modern software development. FastAPI has emerged as the go-to framework for Python developers seeking high performance and developer-friendly features. Combined with PostgreSQL's reliability and advanced features, this stack provides an excellent foundation for production-grade applications.

In this comprehensive guide, we'll walk through creating a complete API system from database design to deployment, covering authentication, testing, and performance optimization strategies that work in real-world scenarios.

Setting Up Your Development Environment

Before diving into code, let's establish a solid development foundation. We'll use Docker for consistent environments and modern Python dependency management.

First, create your project structure:

`bash mkdir fastapi-postgres-api cd fastapi-postgres-api mkdir app tests touch requirements.txt docker-compose.yml `

Your requirements.txt should include:

`txt fastapi==0.104.1 uvicorn[standard]==0.24.0 psycopg2-binary==2.9.7 sqlalchemy==2.0.23 alembic==1.12.1 pydantic[email]==2.5.0 python-jose[cryptography]==3.3.0 passlib[bcrypt]==1.7.4 pytest==7.4.3 httpx==0.25.2 `

Create a docker-compose.yml for local development:

`yaml version: '3.8' services: db: image: postgres:15 environment: POSTGRES_DB: apidb POSTGRES_USER: apiuser POSTGRES_PASSWORD: apipass ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data

api: build: . ports: - "8000:8000" depends_on: - db environment: DATABASE_URL: postgresql://apiuser:apipass@db:5432/apidb volumes: - .:/app command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

volumes: postgres_data: `

This setup provides isolated, reproducible environments and eliminates "works on my machine" issues.

Database Design and SQLAlchemy Integration

Effective database design forms the backbone of any successful API. We'll implement a user management system with proper relationships and constraints.

Create app/database.py:

`python from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker import os

Database URL from environment variable

DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://apiuser:apipass@localhost:5432/apidb")

engine = create_engine(DATABASE_URL) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

Dependency for database sessions

def get_db(): db = SessionLocal() try: yield db finally: db.close() `

Now, define your models in app/models.py:

`python from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Text from sqlalchemy.relationship import relationship from sqlalchemy.sql import func from .database import Base

class User(Base): __tablename__ = "users" id = Column(Integer, primary_key=True, index=True) email = Column(String, unique=True, index=True, nullable=False) hashed_password = Column(String, nullable=False) full_name = Column(String, nullable=False) is_active = Column(Boolean, default=True) is_superuser = Column(Boolean, default=False) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) # Relationship to posts posts = relationship("Post", back_populates="author")

class Post(Base): __tablename__ = "posts" id = Column(Integer, primary_key=True, index=True) title = Column(String, nullable=False) content = Column(Text) published = Column(Boolean, default=False) author_id = Column(Integer, ForeignKey("users.id")) created_at = Column(DateTime(timezone=True), server_default=func.now()) updated_at = Column(DateTime(timezone=True), onupdate=func.now()) # Relationship to user author = relationship("User", back_populates="posts") `

This design includes proper indexing, relationships, and timestamp tracking—essential for production applications.

Building FastAPI Endpoints with Pydantic Models

FastAPI's integration with Pydantic provides automatic request validation, serialization, and API documentation. Let's create comprehensive schemas and endpoints.

Create app/schemas.py:

`python from pydantic import BaseModel, EmailStr, validator from datetime import datetime from typing import List, Optional

User schemas

class UserBase(BaseModel): email: EmailStr full_name: str is_active: bool = True

class UserCreate(UserBase): password: str @validator('password') def validate_password(cls, v): if len(v) < 8: raise ValueError('Password must be at least 8 characters long') return v

class UserUpdate(BaseModel): email: Optional[EmailStr] = None full_name: Optional[str] = None is_active: Optional[bool] = None

class User(UserBase): id: int created_at: datetime updated_at: Optional[datetime] class Config: from_attributes = True

Post schemas

class PostBase(BaseModel): title: str content: Optional[str] = None published: bool = False

class PostCreate(PostBase): pass

class Post(PostBase): id: int author_id: int created_at: datetime author: User class Config: from_attributes = True

Authentication schemas

class Token(BaseModel): access_token: str token_type: str

class TokenData(BaseModel): username: Optional[str] = None `

Now implement the main API in app/main.py:

`python from fastapi import FastAPI, Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from sqlalchemy.orm import Session from typing import List import uvicorn

from . import models, schemas, auth from .database import engine, get_db

Create database tables

models.Base.metadata.create_all(bind=engine)

app = FastAPI( title="FastAPI PostgreSQL API", description="Production-ready API with authentication and CRUD operations", version="1.0.0" )

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")

Authentication endpoints

@app.post("/auth/register", response_model=schemas.User) async def register(user: schemas.UserCreate, db: Session = Depends(get_db)): # Check if user exists db_user = db.query(models.User).filter(models.User.email == user.email).first() if db_user: raise HTTPException( status_code=400, detail="Email already registered" ) # Create new user hashed_password = auth.get_password_hash(user.password) db_user = models.User( email=user.email, hashed_password=hashed_password, full_name=user.full_name, is_active=user.is_active ) db.add(db_user) db.commit() db.refresh(db_user) return db_user

@app.post("/auth/login", response_model=schemas.Token) async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): user = auth.authenticate_user(db, form_data.username, form_data.password) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Incorrect email or password", headers={"WWW-Authenticate": "Bearer"}, ) access_token = auth.create_access_token(data={"sub": user.email}) return {"access_token": access_token, "token_type": "bearer"}

User endpoints

@app.get("/users/me", response_model=schemas.User) async def read_users_me(current_user: models.User = Depends(auth.get_current_user)): return current_user

@app.get("/users/{user_id}", response_model=schemas.User) async def read_user(user_id: int, db: Session = Depends(get_db)): db_user = db.query(models.User).filter(models.User.id == user_id).first() if db_user is None: raise HTTPException(status_code=404, detail="User not found") return db_user

Post endpoints

@app.post("/posts/", response_model=schemas.Post) async def create_post( post: schemas.PostCreate, current_user: models.User = Depends(auth.get_current_user), db: Session = Depends(get_db) ): db_post = models.Post(post.dict(), author_id=current_user.id) db.add(db_post) db.commit() db.refresh(db_post) return db_post

@app.get("/posts/", response_model=List[schemas.Post]) async def read_posts( skip: int = 0, limit: int = 100, db: Session = Depends(get_db) ): posts = db.query(models.Post).filter(models.Post.published == True).offset(skip).limit(limit).all() return posts

@app.get("/posts/{post_id}", response_model=schemas.Post) async def read_post(post_id: int, db: Session = Depends(get_db)): post = db.query(models.Post).filter(models.Post.id == post_id).first() if post is None: raise HTTPException(status_code=404, detail="Post not found") return post

if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000) `

This implementation provides comprehensive CRUD operations with proper error handling and response models.

Implementing JWT Authentication and Security

Security is paramount in production APIs. We'll implement JWT-based authentication with proper password hashing and token management.

Create app/auth.py:

`python from datetime import datetime, timedelta from typing import Optional from jose import JWTError, jwt from passlib.context import CryptContext from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from sqlalchemy.orm import Session

from . import models, schemas from .database import get_db

Security configuration

SECRET_KEY = "your-secret-key-here" # Use environment variable in production ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")

def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password)

def get_password_hash(password): return pwd_context.hash(password)

def get_user_by_email(db: Session, email: str): return db.query(models.User).filter(models.User.email == email).first()

def authenticate_user(db: Session, email: str, password: str): user = get_user_by_email(db, email) if not user: return False if not verify_password(password, user.hashed_password): return False return user

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt

async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)): credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) username: str = payload.get("sub") if username is None: raise credentials_exception token_data = schemas.TokenData(username=username) except JWTError: raise credentials_exception user = get_user_by_email(db, email=token_data.username) if user is None: raise credentials_exception return user

async def get_current_active_user(current_user: models.User = Depends(get_current_user)): if not current_user.is_active: raise HTTPException(status_code=400, detail="Inactive user") return current_user `

This authentication system provides secure password handling, JWT token generation, and user session management.

Testing Your API with Pytest

Comprehensive testing ensures your API works correctly and helps prevent regressions. Let's implement a robust test suite.

Create tests/conftest.py:

`python import pytest from fastapi.testclient import TestClient from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker

from app.main import app from app.database import get_db, Base from app import models

Test database setup

SQLALCHEMY_DATABASE_URL = "postgresql://apiuser:apipass@localhost:5432/test_apidb"

engine = create_engine(SQLALCHEMY_DATABASE_URL) TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

@pytest.fixture(scope="session") def db(): Base.metadata.create_all(bind=engine) yield Base.metadata.drop_all(bind=engine)

@pytest.fixture(scope="function") def db_session(db): connection = engine.connect() transaction = connection.begin() session = TestingSessionLocal(bind=connection) yield session session.close() transaction.rollback() connection.close()

@pytest.fixture(scope="function") def client(db_session): def override_get_db(): try: yield db_session finally: db_session.close() app.dependency_overrides[get_db] = override_get_db yield TestClient(app) app.dependency_overrides.clear() `

Now create tests/test_auth.py:

`python def test_register_user(client): response = client.post( "/auth/register", json={ "email": "test@example.com", "password": "testpass123", "full_name": "Test User" } ) assert response.status_code == 200 data = response.json() assert data["email"] == "test@example.com" assert "hashed_password" not in data

def test_login_user(client): # First register a user client.post( "/auth/register", json={ "email": "test@example.com", "password": "testpass123", "full_name": "Test User" } ) # Then login response = client.post( "/auth/login", data={ "username": "test@example.com", "password": "testpass123" } ) assert response.status_code == 200 data = response.json() assert "access_token" in data assert data["token_type"] == "bearer" `

Run your tests with:

`bash pytest tests/ -v `

This testing setup provides database isolation and comprehensive coverage of your API endpoints.

Production Deployment and Performance Optimization

Deploying FastAPI applications requires attention to performance, security, and scalability. Here are proven strategies for production environments.

Database Connection Pooling

Modify your database.py for production:

`python from sqlalchemy import create_engine from sqlalchemy.pool import QueuePool

engine = create_engine( DATABASE_URL, poolclass=QueuePool, pool_size=20, max_overflow=0, pool_recycle=3600, pool_pre_ping=True ) `

Environment Configuration

Create app/config.py:

`python from pydantic import BaseSettings

class Settings(BaseSettings): database_url: str secret_key: str algorithm: str = "HS256" access_token_expire_minutes: int = 30 class Config: env_file = ".env"

settings = Settings() `

Docker Production Setup

Create a production Dockerfile:

`dockerfile FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt

COPY ./app ./app

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"] `

Performance Monitoring

Add middleware for request timing:

`python import time from fastapi import Request

@app.middleware("http") async def add_process_time_header(request: Request, call_next): start_time = time.time() response = await call_next(request) process_time = time.time() - start_time response.headers["X-Process-Time"] = str(process_time) return response `

Common Pitfalls and Troubleshooting

Avoid these frequent mistakes that can cause production issues:

1. Database Connection Leaks: Always use dependency injection for database sessions 2. Blocking Operations: Use async/await for I/O operations 3. Missing Validation: Implement comprehensive input validation with Pydantic 4. Security Oversights: Never store secrets in code; use environment variables 5. Poor Error Handling: Implement global exception handlers for consistent error responses

For debugging database issues, enable SQLAlchemy logging:

`python import logging logging.basicConfig() logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO) `

Next Steps and Best Practices

To further enhance your FastAPI PostgreSQL application:

1. Implement Rate Limiting: Use slowapi or similar libraries 2. Add Request Validation: Implement comprehensive input sanitization 3. Set Up Monitoring: Integrate with tools like Prometheus and Grafana 4. Database Migrations: Use Alembic for schema version control 5. API Documentation: Leverage FastAPI's automatic OpenAPI documentation 6. Caching Strategy: Implement Redis for frequently accessed data

Building production-ready APIs requires attention to detail, but FastAPI and PostgreSQL provide an excellent foundation. Focus on security, testing, and performance from the start, and your application will scale effectively as your user base grows.

Remember to monitor your application in production, implement proper logging, and maintain comprehensive test coverage as you add new features. The patterns shown in this guide will serve as a solid foundation for any FastAPI project.

Tags

  • API Development
  • Backend
  • FastAPI
  • Python
  • postgresql

Related Articles

Related Books - Expand Your Knowledge

Explore these Python books to deepen your understanding:

Browse all IT books

Popular Technical Articles & Tutorials

Explore our comprehensive collection of technical articles, programming tutorials, and IT guides written by industry experts:

Browse all 8+ technical articles | Read our IT blog