Home Python FastAPI Database Integration with SQLAlchemy

FastAPI Database Integration with SQLAlchemy

⚡ Quick Answer
In FastAPI, you manage database connectivity by injecting a session into your endpoints via the Depends() system. Use a get_db() generator function that yields a session and ensures its closure in a finally block. For production, leverage SQLAlchemy 2.0's AsyncSession with an async driver (like asyncpg) to prevent blocking the FastAPI event loop during I/O-bound database queries.

Async Database Configuration and Session Lifecycle

For modern FastAPI apps, we utilize asyncio. This configuration uses the postgresql+asyncpg driver. The get_db generator is the heart of the integration: it provides a session to your endpoint and guarantees closure, even if the request crashes.

io/thecodeforge/db/session.py · PYTHON
1234567891011121314151617181920212223242526272829303132
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy.orm import DeclarativeBase
import os

# Production-grade PostgreSQL async connection string
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql+asyncpg://user:pass@localhost/forge_db")

# create_async_engine prevents the event loop from blocking during I/O
engine = create_async_engine(DATABASE_URL, echo=False, future=True)

# Factory for creating session objects
AsyncSessionLocal = async_sessionmaker(
    bind=engine, 
    class_=AsyncSession, 
    expire_on_commit=False
)

class Base(DeclarativeBase):
    """Base class for all SQLAlchemy models"""
    pass

# Dependency: The 'Session-per-Request' provider
async def get_db():
    async with AsyncSessionLocal() as session:
        try:
            yield session
            # Session is automatically committed/closed if using 'async with' context
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()
▶ Output
Async connection pool initialized. get_db yields non-blocking sessions.

Models and Async CRUD Operations

In SQLAlchemy 2.0, we use the select() statement approach. This is more explicit and aligns better with static analysis tools like Mypy. Note how we keep the SQLAlchemy Model separate from the Pydantic schema.

io/thecodeforge/db/models.py · PYTHON
1234567891011121314151617181920212223242526272829303132333435
from sqlalchemy import String, Integer, select
from sqlalchemy.orm import Mapped, mapped_column
from fastapi import FastAPI, Depends, HTTPException, status
from io.thecodeforge.db.session import Base, get_db, AsyncSession
from pydantic import BaseModel, EmailStr

# 1. SQLAlchemy Model (Database Identity)
class User(Base):
    __tablename__ = 'users'
    id: Mapped[int] = mapped_column(primary_key=True, index=True)
    username: Mapped[str] = mapped_column(String(50), unique=True)
    email: Mapped[str] = mapped_column(unique=True)

# 2. Pydantic Schema (API Contract)
class UserCreate(BaseModel):
    username: str
    email: EmailStr

app = FastAPI()

@app.post('/forge/users', status_code=status.HTTP_201_CREATED)
async def create_user(payload: UserCreate, db: AsyncSession = Depends(get_db)):
    new_user = User(username=payload.username, email=payload.email)
    db.add(new_user)
    await db.commit()
    await db.refresh(new_user) # Reloads auto-generated ID from DB
    return new_user

@app.get('/forge/users/{user_id}')
async def read_user(user_id: int, db: AsyncSession = Depends(get_db)):
    result = await db.execute(select(User).where(User.id == user_id))
    user = result.scalar_one_or_none()
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user
▶ Output
HTTP 201 Created: {'id': 1, 'username': 'forge_dev', 'email': 'dev@thecodeforge.io'}

🎯 Key Takeaways

  • The get_db() generator is a 'Unit of Work' pattern implementation—ensuring atomic transactions per request.
  • Async Drivers: Always use asyncpg or aiosqlite with FastAPI to avoid stalling the event loop on database waits.
  • SQLAlchemy 2.0 Syntax: Prefer select(Model) over the legacy session.query(Model) for better type safety and future-proofing.
  • Model Separation: Pydantic handles the 'JSON-to-Object' conversion; SQLAlchemy handles the 'Object-to-SQL' conversion.
  • Expiration: Set expire_on_commit=False in your session factory to prevent 'greenlet' errors when accessing attributes after a commit in async contexts.

Interview Questions on This Topic

  • QWhat is the 'N+1 Problem' in SQLAlchemy, and how do you use `joinedload` or `selectinload` in FastAPI to solve it?
  • QExplain the internal mechanics of how `Depends(get_db)` ensures the database session is closed even if the endpoint raises an unhandled exception.
  • QIn a high-concurrency FastAPI app, what are the dangers of using a synchronous SQLAlchemy engine? How does it affect the Uvicorn worker?
  • QHow would you implement a Global Transaction Middleware so that every request is automatically wrapped in a transaction that rolls back on any 4xx or 5xx error?
  • QDescribe the difference between `expire_on_commit=True` vs `False`. Why is `False` often preferred in asynchronous FastAPI applications?

Frequently Asked Questions

Why do I need db.refresh() after db.commit()?

When you call commit(), SQLAlchemy marks the local object as 'expired' because the database state is now the ultimate source of truth (it might have applied triggers, auto-increments, or default timestamps). refresh() tells SQLAlchemy to perform a SELECT immediately to pull those database-generated values back into your Python object so you can use them in your API response.

Should I use SQLAlchemy ORM or Core with FastAPI?

At TheCodeForge, we use the ORM for 90% of tasks because of its productivity and relationship mapping. However, for high-performance bulk inserts or complex reporting queries involving heavy window functions, we drop down to SQLAlchemy Core (SQL Expressions) to bypass the overhead of object instantiation.

How do I handle database migrations in FastAPI?

FastAPI/SQLAlchemy do not handle migrations automatically. The industry standard is Alembic. You use Alembic to track changes to your Base models and generate migration scripts (like upgrade and downgrade) to keep your production schema in sync with your code.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousFastAPI Background Tasks and Async EndpointsNext →FastAPI File Uploads and Form Data
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged