Web Dev

Why We Use the Repository Pattern in Every FastAPI Project

The repository pattern keeps FastAPI backends testable, swappable, and clean. Here is how we structure data access in production Python APIs — with code.

TTaimoorAI & Backend Engineer
6 min read
W

FastAPI makes it dangerously easy to query the database directly inside your route handlers. It works — until you need to write a test, swap an ORM, or reuse a query. The repository pattern is the small dose of discipline that keeps a growing codebase from turning into spaghetti.

What the repository pattern actually is

A repository is a class that mediates between your domain logic and the data source. Your services talk to an interface like get_user(id) and never see SQLAlchemy sessions or raw SQL. The data layer becomes a detail you can replace without touching business logic — the heart of clean architecture.

The shape of it

from typing import Protocol
from sqlalchemy.orm import Session

class UserRepository(Protocol):
    def get(self, user_id: int) -> User | None: ...
    def add(self, user: User) -> User: ...

class SqlUserRepository:
    def __init__(self, db: Session):
        self.db = db

    def get(self, user_id: int) -> User | None:
        return self.db.query(User).filter(User.id == user_id).first()

    def add(self, user: User) -> User:
        self.db.add(user)
        self.db.commit()
        return user

Wiring it with FastAPI dependencies

FastAPI’s dependency injection makes the swap trivial. Routes depend on the interface; you decide the concrete implementation at the edge.

def get_user_repo(db: Session = Depends(get_db)) -> UserRepository:
    return SqlUserRepository(db)

@app.get("/users/{user_id}")
def read_user(user_id: int, repo: UserRepository = Depends(get_user_repo)):
    user = repo.get(user_id)
    if not user:
        raise HTTPException(404)
    return user

The payoff

  • Testability — inject an in-memory fake repository and test services with zero database.
  • Swappability — move from Postgres to a read replica, or add caching, without touching routes.
  • Reusability — one place owns each query, so behavior never drifts between endpoints.

When not to bother

For a throwaway prototype or a three-endpoint internal tool, direct queries are fine. The pattern pays off when a project has real domain logic and a lifespan measured in years — which is most of what we build for clients.

Key takeaways

  • Keep ORM details out of your route handlers.
  • Depend on interfaces; choose implementations at the edge.
  • Reach for it when logic and longevity justify the structure.

This is one of the conventions behind our web & backend development work. Curious how we would structure your API? Let’s talk. You might also like our take on JWT auth in FastAPI.

All articlesWritten by Taimoor · AI & Backend Engineer

Have a project that needs this kind of engineering?

We turn ideas into production software — AI, web, mobile, and cloud.

Start a Conversation