FastCRUD for FastAPI: Less Repetitive CRUD, Not Less Architecture

One FastAPI tool I’ve found genuinely useful lately is FastCRUD. More specifically, it is a practical way to reduce the repetitive work around FastCRUD for FastAPI projects that use SQLAlchemy.

When you are building a conventional API with FastAPI and SQLAlchemy, the repeated work adds up quickly. Every new resource can mean creating a record, listing records, fetching one by ID, updating it, deleting it, and then adding filtering, sorting, pagination, and response models.

FastCRUD can generate those standard endpoints from a SQLAlchemy model and Pydantic schemas. It reduces repeated transport and data-access wiring, while leaving room to configure the router or write explicit endpoints where the domain needs them.

What FastCRUD for FastAPI automates well

A small router setup can provide the standard CRUD surface for a resource:

from fastapi import FastAPI
from fastcrud import crud_router

from app.database import get_session
from app.models import Game
from app.schemas import GameCreate, GameRead, GameUpdate

app = FastAPI()

game_router = crud_router(
    session=get_session,
    model=Game,
    create_schema=GameCreate,
    update_schema=GameUpdate,
    select_schema=GameRead,
    path="/games",
    tags=["Games"],
)

app.include_router(game_router)

That does not mean every API should expose every operation. FastCRUD lets you tailor generated routes, but its value is most obvious when a resource really does follow predictable CRUD behaviour.

It also supports query behaviour that often becomes repetitive across endpoints. A list request can use a sort parameter such as:

GET /games?sort=-year,title

In this example, newer games appear first, followed by title in ascending order. The library also supports filtering and offset or cursor-based pagination, which makes it useful for ordinary management interfaces and database-backed services.

Relationship handling in FastCRUD for FastAPI

The capability I found most interesting is relationship handling. Imagine that each game belongs to a designer:

from sqlalchemy import ForeignKey, Integer, String
from sqlalchemy.orm import Mapped, mapped_column, relationship

class Game(Base):
    __tablename__ = "games"

    id: Mapped[int] = mapped_column(primary_key=True)
    title: Mapped[str] = mapped_column(String)
    year: Mapped[int] = mapped_column(Integer)
    designer_id: Mapped[int] = mapped_column(ForeignKey("designers.id"))

    designer: Mapped["Designer"] = relationship(back_populates="games")

With an explicit relationship configuration, FastCRUD can include the designer in read responses:

game_router = crud_router(
    session=get_session,
    model=Game,
    create_schema=GameCreate,
    update_schema=GameUpdate,
    select_schema=GameRead,
    include_relationships=["designer"],
    nest_joins=True,
    path="/games",
)

The client can then receive useful nested data rather than having to make a separate request for every designer:

{
  "id": 1,
  "title": "Catan",
  "year": 1995,
  "designer": {
    "id": 1,
    "name": "Klaus Teuber",
    "country": "Germany"
  }
}
FastCRUD for FastAPI architecture diagram showing SQLAlchemy models and Pydantic schemas feeding a FastCRUD router, generated CRUD endpoints, and the application responsibilities that remain explicit.
FastCRUD can automate the CRUD surface while permissions, tenant boundaries, response shape, relationship limits, business rules, and transactions remain deliberate design choices.

That is helpful, but it is also where the design decisions begin.

Related data should be deliberate

Relationships are not something to expose simply because they exist in the database. FastCRUD does not include them automatically by default, and one-to-many relationships need particular care. Returning every order for a customer, every event for an account, or every game for a designer can make a response unexpectedly large and inefficient.

A better approach is to choose the relationship intentionally, expose only fields that are useful to the client, and apply limits where appropriate. The FastCRUD relationship documentation makes this distinction clear: automatic inclusion is configurable, and one-to-many relations are treated more cautiously because they can grow without a natural bound.

Boilerplate is not the same as architecture

Tools like FastCRUD are strongest when an API is genuinely resource-oriented. They remove repeated code around a conventional data model, which gives developers more time to focus on the parts that actually distinguish the product.

Those parts still need to be designed:

  • Which fields can a client read or write?
  • How do authentication and authorisation work?
  • How are tenant boundaries enforced?
  • Which related records are safe and useful to expose?
  • Where do business rules and side effects belong?
  • When is a purpose-built query or explicit endpoint clearer?

For straightforward management APIs, FastCRUD can remove a substantial amount of plumbing. For workflows involving approvals, payments, permissions, multi-step transactions, or domain-specific rules, I would still prefer explicit endpoints backed by a service layer.

That distinction aligns with the broader principles behind clean architecture: the framework and database boundary should support the domain, not quietly become the domain.

Less boilerplate does not mean less design.

Leave A comment

Are you human? Please solve:Captcha