Clean architecture principles in FastAPI + SQLModel development

3k Views Asked by At

When I develop with Flask and SQLAlchemy, I commonly use the following layers:

  • A repository layer, that deals with database connections and data persistence;
  • A use cases layers, a bunch of functions that use the repository and do exactly what the application is intented to do;
  • A rest layer where my REST endpoints where located.

I get this architectural pattern from the book "Clean Architectures in Python", by Leonardo Giordani. You can get the book here.

Recently I started to learn FastAPI and SQLModel ORM. The whole idea of using Pydantic in SQLModel and FastAPI create the possibility to do an endpoint like this:

@app.get("/songs", response_model=list[Song])
def get_songs(session: Session = Depends(get_session)):
    result = session.execute(select(Song))
    songs = result.scalars().all()
    return [Song(name=song.name, artist=song.artist, id=song.id) for song in songs]

In this endpoint the very database logic is dealt with, as the endpoint receives a session as a dependency and executes a query to get the results and, using Pydantic's magic, receives parsed and validated data and responds with a Pydantic model.

My question is: something like this is considered an clean architecture? Based on the principles I've learn, we have the web framework doing almost everything. And I think the very idea of using Pydantic both in SQLModel and FastAPI is to allow us to create endpoints like that.

But this is clean? When my app grows, couldn't I be limited by this kind of design?

3

There are 3 best solutions below

1
On

Your question is worth.

3 layer architecture is not only in your mentioned book, it is very common over web application.

Your code seems simple and looks like no need to be layered. It does not mean FastAPI are not for 3 layer arch. It means your sample code is too simple, it is just CRUD: read model from db, covert it to set of pydantic instance and return it. If your api only need CRUD data on single Table then your code is perfect for purpose.

On most backend, as business grow, api logic get bigger and more complexed. Api will deal with one or more Table, and need complicated logic. If you keep this kind of code, your code will lose efficient.

Let me show simple example,


### when layered

from ../persistence import song_repository # persistance layer
from ../use_case import recommend_songs    # use case layer


@app.get("/songs", response_model=list[Song])
def get_songs(session: Session = Depends(get_session)):
    # data load goes to song_repository's method.
    songs:list[Song] = song_repository.get_all(session)
    return songs 


@app.get("/recommend/songs", response_model=list[DiscountPrice])
def get_recommend_songs(session: Session = Depends(get_session)):
    # you can reuse song_repository's method
    songs:list[Song] = song_repository.get_all(session)
    recommend_rule: list[Rule]= rule_repository.get_all(session) 
    recommended: list[Song] = recommend_songs(songs, recommend_rule)
    return recommended

### when logic mixed on one path operation

@app.get("/recommend/songs", response_model=list[Song])
def get_songs(session: Session = Depends(get_session)):
    # you load data again!
    result = session.execute(select(Song))
    songs = result.scalars().all()
    data = [Song(name=song.name, artist=song.artist, id=song.id) for song in songs]

    result2 = session.execute(select(Rule))
    rules = result2.scalars().all()
    data2 = [Rule(...) for rule in rules]

    #now you start to describe recommend logic,
    # blah blah but this can not reusable, only exist in this function
    recommended = ...
    
    return recommended
0
On

What I'm questioning is this: if I use a 3 layer structure with FastAPI + SQLModel, wouldn't I throwing away all the benefits to work with these 2 Pydantic wrappers?

In short, no.

Fastapi use pydantic model for checking input and output, they does not care logic inside path function. This is just what rest layer do.

And sql model(maybe sqlalchemy) use pydantic to transfer sql model instance data as Data Transfer Object or serializing it. You misused same class Song for load data and response schema.

@app.get("/songs", response_model=list[SongSchema]) # this is pydantic class
def get_songs(session: Session = Depends(get_session)):
    result = session.execute(select(Song)) # This is SqlAlchemy class
    songs = result.scalars().all()
    return songs # You don't need to serializing it because pydantic will do. If SongScheam has orm = True on Config. 

3 layer arch is bigger than fastapi, sqlmodel and pydantic, so it is not affected by them, but affected them.

Actually, fastapi, pydantic help making 3 layer better than flask. Rest layer became easy to write and comprehensive compare to flask. So does on persistence layer. And this give developer time to focus on service layer.

0
On

I think a lot of the three layer advantage is for larger applications (complexity of app or complexity of dev team). As the other answers have indicated, for trivial applications with little in the way of non-functional requirements you won't really see much of an advantage.

That said, in my view:

The advantages of doing three layer are that many things become easier to do either correctly or efficiently. Examples:

  • Don't need to worry about duplicating lots of almost-the-same code in many places, but can abstract out the differences
  • Caching of expensive queries across multiple use cases
  • AppSec considerations are more easily managed
  • You can swap out things for better things more easily, and maybe abstract schema version changes more.

The primary disadvantage of three layer is that in some cases you could get away with less typing, and unless you are very disciplined will skip a layer for the simple things. If you are relying on the different layers to manage different concerns security-wise, you might therefore allow things unintentionally. Particularly if you are doing a lot of inheritence or decorators in your use case layer, so it isn't quite as visible.

The primary advantage of focusing only on use case functions (abstract the REST via the decorator, do the storage using the ORM inside the same functions) besides typing less is that you can be very clear what is happening in a relatively smaller application, at some loss in efficiency and extensibility. So, prioritizing readability for simple things.

The primary disadvantage of that consists of the reverse of the advantages of three layer. This is even more of a concern when you have multiple developers working on the app.

Bit more detail on the AppSec things you can do in three layer because they often get lost in considerations:

  • logging of the attributes of the request, use case, and data changes happen independently and record the flow through the layers of the application (though hopefully with a common request ID and use case ID passed down!)
  • checking authorization can happen in at least two layers, checking whether the user has the role to perform both the request and the use cases that it drives