FastAPI SQLAlchemy Integration
Introduction
When building web applications with FastAPI, you'll often need to interact with a database to store and retrieve data. While FastAPI doesn't come with a built-in ORM (Object-Relational Mapper), it integrates seamlessly with SQLAlchemy, one of Python's most popular and powerful ORM libraries.
In this guide, we'll explore how to:
- Set up SQLAlchemy with FastAPI
- Define database models
- Perform CRUD operations (Create, Read, Update, Delete)
- Implement best practices for database connections
By the end of this tutorial, you'll have a solid foundation for building database-powered FastAPI applications.
Prerequisites
Before starting, make sure you have:
- Basic knowledge of Python
- Understanding of FastAPI fundamentals
- A Python environment with FastAPI installed
Let's install the necessary packages:
pip install fastapi sqlalchemy alembic psycopg2-binary uvicorn
fastapi
: Our web frameworksqlalchemy
: The ORM we'll usealembic
: For database migrations (optional but recommended)psycopg2-binary
: PostgreSQL driver (you might use a different one based on your database)uvicorn
: ASGI server to run our FastAPI application
Setting Up SQLAlchemy with FastAPI
Step 1: Database Configuration
First, let's create a structure for our application:
my_fastapi_app/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── database.py
│ ├── models.py
│ └── schemas.py
└── requirements.txt
Now, let's set up our database connection in database.py
:
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
# Change this to your database URL
SQLALCHEMY_DATABASE_URL = "postgresql://username:password@localhost/db_name"
# For SQLite, you would use:
# SQLALCHEMY_DATABASE_URL = "sqlite:///./sql_app.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
# Create SessionLocal class
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Create a Base class for models
Base = declarative_base()
# Dependency to get DB session
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
Step 2: Define Models
Next, let's create our database models in models.py
:
from sqlalchemy import Boolean, Column, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from .database import Base
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
email = Column(String, unique=True, index=True)
hashed_password = Column(String)
is_active = Column(Boolean, default=True)
# Define relationship with Item model
items = relationship("Item", back_populates="owner")
class Item(Base):
__tablename__ = "items"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
description = Column(String)
owner_id = Column(Integer, ForeignKey("users.id"))
# Define relationship with User model
owner = relationship("User", back_populates="items")
Step 3: Create Pydantic Models (Schemas)
In schemas.py
, we'll define Pydantic models for data validation and serialization:
from typing import List, Optional
from pydantic import BaseModel
class ItemBase(BaseModel):
title: str
description: Optional[str] = None
class ItemCreate(ItemBase):
pass
class Item(ItemBase):
id: int
owner_id: int
# Enable ORM mode
class Config:
orm_mode = True
class UserBase(BaseModel):
email: str
class UserCreate(UserBase):
password: str
class User(UserBase):
id: int
is_active: bool
items: List[Item] = []
# Enable ORM mode
class Config:
orm_mode = True
Step 4: Create the FastAPI Application
Now, let's create the FastAPI application in main.py
:
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session
from . import models, schemas
from .database import get_db, engine
# Create database tables
models.Base.metadata.create_all(bind=engine)
app = FastAPI()
@app.post("/users/", response_model=schemas.User)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
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")
# In a real app, hash the password
fake_hashed_password = user.password + "notreallyhashed"
db_user = models.User(email=user.email, hashed_password=fake_hashed_password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
@app.get("/users/", response_model=list[schemas.User])
def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
users = db.query(models.User).offset(skip).limit(limit).all()
return users
@app.get("/users/{user_id}", response_model=schemas.User)
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
@app.post("/users/{user_id}/items/", response_model=schemas.Item)
def create_item_for_user(
user_id: int, item: schemas.ItemCreate, 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")
db_item = models.Item(**item.dict(), owner_id=user_id)
db.add(db_item)
db.commit()
db.refresh(db_item)
return db_item
@app.get("/items/", response_model=list[schemas.Item])
def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
items = db.query(models.Item).offset(skip).limit(limit).all()
return items
Running the Application
To run your application, execute:
uvicorn app.main:app --reload
Testing the Endpoints
Creating a User
Request:
POST /users/
Content-Type: application/json
{
"email": "[email protected]",
"password": "password123"
}
Response:
{
"id": 1,
"email": "[email protected]",
"is_active": true,
"items": []
}
Creating an Item for a User
Request:
POST /users/1/items/
Content-Type: application/json
{
"title": "My first item",
"description": "This is an example item"
}
Response:
{
"id": 1,
"title": "My first item",
"description": "This is an example item",
"owner_id": 1
}
Best Practices
1. Use Dependency Injection
FastAPI's dependency injection system is perfect for managing database sessions:
def get_user(user_id: int, db: Session = Depends(get_db)):
user = db.query(models.User).filter(models.User.id == user_id).first()
if user is None:
raise HTTPException(status_code=404, detail="User not found")
return user
@app.get("/users/{user_id}")
def read_user(user: models.User = Depends(get_user)):
return user
2. Organize Large Applications
For larger applications, consider organizing your code by feature:
app/
├── api/
│ ├── __init__.py
│ ├── dependencies.py
│ └── endpoints/
│ ├── __init__.py
│ ├── users.py
│ └── items.py
├── core/
│ ├── __init__.py
│ └── config.py
├── db/
│ ├── __init__.py
│ ├── base.py
│ └── session.py
├── models/
│ ├── __init__.py
│ ├── user.py
│ └── item.py
└── schemas/
├── __init__.py
├── user.py
└── item.py
3. Use Alembic for Migrations
As your database schema evolves, use Alembic to manage migrations:
# Initialize Alembic
alembic init alembic
# Create a migration
alembic revision --autogenerate -m "Create users and items tables"
# Apply the migration
alembic upgrade head
4. Implement Pagination
For endpoints that return multiple items, always implement pagination:
@app.get("/users/", response_model=list[schemas.User])
def read_users(
skip: int = 0,
limit: int = 100,
db: Session = Depends(get_db)
):
users = db.query(models.User).offset(skip).limit(limit).all()
return users
Real-World Example: Blog API
Let's create a simple blog API to demonstrate a more complete example:
Models
# models.py
from sqlalchemy import Column, ForeignKey, Integer, String, Text, DateTime
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from .database import Base
class Author(Base):
__tablename__ = "authors"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, index=True)
email = Column(String, unique=True, index=True)
bio = Column(Text, nullable=True)
posts = relationship("Post", back_populates="author")
class Post(Base):
__tablename__ = "posts"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
content = Column(Text)
published = Column(DateTime(timezone=True), server_default=func.now())
author_id = Column(Integer, ForeignKey("authors.id"))
author = relationship("Author", back_populates="posts")
comments = relationship("Comment", back_populates="post")
class Comment(Base):
__tablename__ = "comments"
id = Column(Integer, primary_key=True, index=True)
content = Column(Text)
published = Column(DateTime(timezone=True), server_default=func.now())
post_id = Column(Integer, ForeignKey("posts.id"))
post = relationship("Post", back_populates="comments")
Schemas
# schemas.py
from datetime import datetime
from typing import List, Optional
from pydantic import BaseModel, EmailStr
# Comment schemas
class CommentBase(BaseModel):
content: str
class CommentCreate(CommentBase):
pass
class Comment(CommentBase):
id: int
published: datetime
post_id: int
class Config:
orm_mode = True
# Post schemas
class PostBase(BaseModel):
title: str
content: str
class PostCreate(PostBase):
pass
class Post(PostBase):
id: int
published: datetime
author_id: int
comments: List[Comment] = []
class Config:
orm_mode = True
# Author schemas
class AuthorBase(BaseModel):
name: str
email: EmailStr
bio: Optional[str] = None
class AuthorCreate(AuthorBase):
pass
class Author(AuthorBase):
id: int
posts: List[Post] = []
class Config:
orm_mode = True
API Routes
# main.py
from fastapi import Depends, FastAPI, HTTPException
from sqlalchemy.orm import Session
from typing import List
from . import models, schemas
from .database import get_db, engine
models.Base.metadata.create_all(bind=engine)
app = FastAPI(title="Blog API")
# Author routes
@app.post("/authors/", response_model=schemas.Author)
def create_author(author: schemas.AuthorCreate, db: Session = Depends(get_db)):
db_author = models.Author(**author.dict())
db.add(db_author)
db.commit()
db.refresh(db_author)
return db_author
@app.get("/authors/", response_model=List[schemas.Author])
def read_authors(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
authors = db.query(models.Author).offset(skip).limit(limit).all()
return authors
@app.get("/authors/{author_id}", response_model=schemas.Author)
def read_author(author_id: int, db: Session = Depends(get_db)):
db_author = db.query(models.Author).filter(models.Author.id == author_id).first()
if db_author is None:
raise HTTPException(status_code=404, detail="Author not found")
return db_author
# Post routes
@app.post("/authors/{author_id}/posts/", response_model=schemas.Post)
def create_post(
author_id: int, post: schemas.PostCreate, db: Session = Depends(get_db)
):
db_post = models.Post(**post.dict(), author_id=author_id)
db.add(db_post)
db.commit()
db.refresh(db_post)
return db_post
@app.get("/posts/", response_model=List[schemas.Post])
def read_posts(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
posts = db.query(models.Post).offset(skip).limit(limit).all()
return posts
@app.get("/posts/{post_id}", response_model=schemas.Post)
def read_post(post_id: int, db: Session = Depends(get_db)):
db_post = db.query(models.Post).filter(models.Post.id == post_id).first()
if db_post is None:
raise HTTPException(status_code=404, detail="Post not found")
return db_post
# Comment routes
@app.post("/posts/{post_id}/comments/", response_model=schemas.Comment)
def create_comment(
post_id: int, comment: schemas.CommentCreate, db: Session = Depends(get_db)
):
db_comment = models.Comment(**comment.dict(), post_id=post_id)
db.add(db_comment)
db.commit()
db.refresh(db_comment)
return db_comment
Summary
In this guide, we've covered:
- Setting up SQLAlchemy with FastAPI
- Defining database models and Pydantic schemas
- Creating CRUD operations using FastAPI endpoints
- Implementing best practices for database operations
- Building a real-world example of a blog API
SQLAlchemy integration enables FastAPI applications to interact with relational databases efficiently. The combination provides a robust foundation for building high-performance, type-safe APIs with database persistence.
Further Reading and Exercises
Additional Resources
- SQLAlchemy Documentation
- FastAPI Database Tutorial
- Alembic Documentation for database migrations
Exercises
- Extend the Blog API: Add functionality for categories and tags for blog posts
- Implement Authentication: Add user authentication using JWT or OAuth2
- Add Filtering and Sorting: Enhance the list endpoints with filtering and sorting capabilities
- Implement Soft Delete: Instead of deleting records, mark them as inactive
- Create Admin Dashboard: Build a simple admin interface to manage blog content
By completing these exercises, you'll gain hands-on experience with FastAPI and SQLAlchemy integration, preparing you for building real-world applications.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)