Building web applications with FastAPI and SQLAlchemy can be a smooth ride if you get the right pieces in place from the get-go. Both tools are powerful in their own right, and when combined, they can take your app development journey to new heights. This guide will walk you through integrating SQLAlchemy with FastAPI, ensuring your app is both high-performing and easy to maintain.
Setting Up the Basics
First things first, you need to get your development environment ready. Python 3.7 or later is essential. You might want to use pipenv
for handling your virtual environment, as it simplifies package management significantly. Use the following command to install pipenv
:
pip install pipenv
Once that’s out of the way, create and activate your virtual environment:
pipenv shell
Now, you’ll need to install essential packages. Fire up these commands:
pipenv install fastapi uvicorn sqlalchemy alembic
Organizing Your Project
Keeping your project well-organized is crucial for future maintainability. Here’s a solid structure you can follow:
project/
│
├── main.py
├── models/
│ ├── __init__.py
│ ├── user.py
│ └── task.py
├── schemas/
│ ├── __init__.py
│ ├── user.py
│ └── task.py
├── crud/
│ ├── __init__.py
│ ├── user.py
│ └── task.py
├── database.py
├── alembic.ini
├── alembic/
│ ├── env.py
│ ├── script.py.mako
│ └── versions/
└── requirements.txt
Configuring SQLAlchemy
In your database.py
file, you’ll need to set up the SQLAlchemy engine and session. This setup manages the connection to your database.
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, scoped_session
from sqlalchemy.orm.session import sessionmaker
SQLALCHEMY_DATABASE_URL = "sqlite:///test.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
Defining Your Models
Next, it’s time to declare your SQLAlchemy models, which represent tables in your database. For instance, here’s a User
model:
# models/user.py
from sqlalchemy import Column, Integer, String
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)
def __repr__(self):
return f"User(id={self.id}, email={self.email})"
A Task
model might look like this:
# models/task.py
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import relationship
from database import Base
from models.user import User
class Task(Base):
__tablename__ = "tasks"
id = Column(Integer, primary_key=True, index=True)
title = Column(String, index=True)
description = Column(String, index=True)
owner_id = Column(Integer, ForeignKey("users.id"))
owner = relationship("User", backref="tasks")
def __repr__(self):
return f"Task(id={self.id}, title={self.title}, description={self.description})"
Making Your Database Tables
With Alembic, managing database migrations is a breeze. Initialize it using the following command:
alembic init alembic
Update your alembic.ini
with your database URL, and then create and apply your first migration:
alembic revision --autogenerate -m "Initial migration"
alembic upgrade head
Defining Schemas with Pydantic
Pydantic plays a crucial role in data validation. Define schemas for your models like so:
# schemas/user.py
from pydantic import BaseModel
class UserCreate(BaseModel):
email: str
password: str
class User(BaseModel):
id: int
email: str
class Config:
orm_mode = True
# schemas/task.py
from pydantic import BaseModel
class TaskCreate(BaseModel):
title: str
description: str
class Task(BaseModel):
id: int
title: str
description: str
owner_id: int
class Config:
orm_mode = True
CRUD Operations
Create your CRUD operations within a dedicated crud
module. Here’s how you can handle users:
# crud/user.py
from sqlalchemy.orm import Session
from models.user import User
from schemas.user import UserCreate
def get_user(db: Session, user_id: int) -> User:
return db.query(User).filter(User.id == user_id).first()
def get_user_by_email(db: Session, email: str) -> User:
return db.query(User).filter(User.email == email).first()
def create_user(db: Session, user: UserCreate) -> User:
db_user = User(email=user.email, hashed_password=user.password)
db.add(db_user)
db.commit()
db.refresh(db_user)
return db_user
And for tasks:
# crud/task.py
from sqlalchemy.orm import Session
from models.task import Task
from schemas.task import TaskCreate
def get_task(db: Session, task_id: int) -> Task:
return db.query(Task).filter(Task.id == task_id).first()
def create_task(db: Session, task: TaskCreate, owner_id: int) -> Task:
db_task = Task(title=task.title, description=task.description, owner_id=owner_id)
db.add(db_task)
db.commit()
db.refresh(db_task)
return db_task
FastAPI Integration
Time to put it all together within your FastAPI application:
# main.py
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from database import SessionLocal, engine
from crud.user import create_user, get_user_by_email
from crud.task import create_task
from models.user import User
from models.task import Task
from schemas.user import UserCreate, User
from schemas.task import TaskCreate, Task
app = FastAPI()
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.post("/users/", response_model=User)
def create_new_user(user: UserCreate, db: Session = Depends(get_db)):
db_user = get_user_by_email(db, email=user.email)
if db_user:
raise HTTPException(status_code=400, detail="Email already registered")
return create_user(db=db, user=user)
@app.post("/users/{user_id}/tasks/", response_model=Task)
def create_task_for_user(user_id: int, task: TaskCreate, db: Session = Depends(get_db)):
db_user = get_user(db, user_id=user_id)
if not db_user:
raise HTTPException(status_code=404, detail="User not found")
return create_task(db=db, task=task, owner_id=user_id)
@app.get("/users/{user_id}/tasks/", response_model=list[Task])
def read_tasks_for_user(user_id: int, db: Session = Depends(get_db)):
db_user = get_user(db, user_id=user_id)
if not db_user:
raise HTTPException(status_code=404, detail="User not found")
return db.query(Task).filter(Task.owner_id == user_id).all()
Managing Database Sessions
Handling database sessions properly can be a bit tricky. Here’s a middleware to manage the session lifecycle.
@app.middleware("http")
async def db_session_middleware(request: Request, call_next):
response = Response("Internal server error", status_code=500)
try:
request.state.db = SessionLocal()
response = await call_next(request)
finally:
request.state.db.close()
return response
Testing Your Application
Testing is essential. Use Pytest for writing tests and ensuring your app behaves as expected.
# tests/test_main.py
from fastapi.testclient import TestClient
from main import app
from database import SessionLocal, engine
client = TestClient(app)
def override_get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
def test_create_user():
response = client.post("/users/", json={"email": "[email protected]", "password": "password"})
assert response.status_code == 200
assert response.json()["email"] == "[email protected]"
def test_create_task_for_user():
user_response = client.post("/users/", json={"email": "[email protected]", "password": "password"})
user_id = user_response.json()["id"]
task_response = client.post(f"/users/{user_id}/tasks/", json={"title": "Test Task", "description": "This is a test task"})
assert task_response.status_code == 200
assert task_response.json()["title"] == "Test Task"
Best Practices
-
Separate Concerns: Keep your database models and API endpoints separate. Use Pydantic models for input validation and SQLAlchemy models for database interactions.
-
Utilize Repositories: Implement the Repository pattern for encapsulating database operations. This helps in uncoupling your business logic from database interactions.
-
Migration Management: Use Alembic to handle database migrations, ensuring that your database schema stays in sync with your application models.
-
Dependency Injection: Use dependency injection to manage database sessions. This guarantees that each request has its own session and that it’s properly closed after processing.
-
Thorough Testing: Write extensive tests to validate your application’s behavior. FastAPI’s test clients can simulate requests and help test your endpoints efficiently.
By sticking to these guidelines, you’ll be on your way to developing robust and maintainable FastAPI applications integrated with SQLAlchemy, delivering excellent performance and scalability.