The Price of Freedom
Given all the cool features that FastAPI packs in for you, one might be surprised to see complaints about the freedom to choose your own project organization. What some people call “limiting”, other might find “good patterns to be followed for a gain in productivity”. That said, this is just a suggestion of how one can organize the FastAPI project in order to still respect some good design patterns.
Project
There is of course one “official” approach to the matter in this section of the Lib website when it comes to larger projects. This is what it looks like:
One important thing to notice is that the app folder is not necessarily your root folder. Personally, I like to have an app
for the core app, and another tests
folder which contain code related to tests. The tests
folder may contain subfolders like unit
(for unit tests), integration
, etc.
If you will use Docker with your app (which is very likely), the Dockerfile can be in the root, and when you build your docker image, you will simply copy all the contents of your app
folder into the container.
|-- app
|-- api
|-- models
|-- requirements.txt
|-- tests
|-- unit
|-- requirements.txt
|-- integration
|-- requirements.txt
|-- Dockerfile
|-- docker-compose.yaml
|-- cicd_config(Github/Gitlab/Bitbucket)
|-- sample.env
Keep in mind that a project never starts big though: it grows as needed, and that is when things get messy because sometimes the growth has no order or proper structure and maintainers realize the structure needs to change too late. Therefore, checking if the project will need structural changes is a regular task.
Just like Flask Blueprints, we can split the project into smaller modules (usually divided by Domain) that represent a section of code that share some common features in logic and context.
Doing some research on Google, we find some interesting resources like that lib called manage-fastapi whose only function is to create the backbone for your FastAPI project. I personally like this structure because it takes into account versioning ( what the next section is dedicated to).
API Versioning and Build Versioning
One of the most important parts of deciding the structure of your project is related to maintenance and future changes. Versioning APIs will make your application backward compatible with older clients.
Let’s say your backend serves a browser app, an iOS app and an android app. If you have only one api version, you will have to make sure all clients are updated at the same time you deploy a breaking change. This is practically impossible right?
We can start with version v1.0, and include subfolders for minor version changes, like this:
|-- app
|-- api
|-- v1
|-- 0
|-- schemas.py
|-- routes.py
|-- 1
|-- ...
… or you can create one folder per major-minor version:
|-- app
|-- api
|-- v1_0
|-- schemas.py
|-- routes.py
|-- v1_1
|-- ...
It is important to start your project with this version so your URLs already have versioning in them. There are two main possibilities for versioning: in the URL address, or as a header parameter. Personally, I like the URL option because it will probably make debugging and tracking errors in the future easier with the cloud provider logging tool (which usually records URLs but may require aditional configuration to track header contents).
The Controller/Service Layer
Besides the motivation of easy code replacement for different implementations of the same functionality, one factor that might influence your project are better ways to test it.
Separating your code into a “controller/service” layer is a way to make the business logic reusable in different API versions. Suppose you have a service to create a user that requires a name, address, and phone in v1.0, and after some years of using this endpoint you realize you now want to separate the phone into a new section that has the number, model, and which carrier the user has. The logic to store the phone into the DB can have those new fields as optional, and be compatible with both versions.
A smaller motivator for the consideration of package creation and the definition of a “service” layer comes with the processing power of IoT devices. Yes, this does not look like something relevant at the moment but consider the idea of easily packing the business logic from your service into a module to be used directly in devices. If the REST interface and the logic are not already separated, this will require a large effort. The existence of a “service” or “controller” layer might sound so 2000’s to some of you (and that reminds me of how old I am getting 😓 ), but it is still useful.
Payloads
Believe it or not, some good patterns exist to follow even for your REST API JSON payloads. The reason is somehow simple: payloads will naturally change as your application evolves and gains or loses some functionalities, and you want to follow good “rules of thumb” to avoid breaking backward compatibility.
Some good references when in doubt are Google JSON Style Guide and JSON API Org. In the next lines I share some points I think are the most important.
First is that adding new fields is always easier and less prompt to error than removing fields (or renaming fields, which can be seen as a removal followed by an addition). So, whenever possible, create larger, generic objects inside your payload, because they can be easily filled and if needed, fields can be replicated in different objects.
A good start is to always have at least those 3 params in the root of your payload:
{
"data": ...,
"error": ...,
"meta": ...
}
A succesfull response will not have an error
param, and a failed request will not have a data
one.
The meta
param is useful to include information not related to the contents of the response, but for pagination or performance details (like the time used to process the response) for example.
Conclusion
As pointed out before, all those are just suggestions of code organization for the Fast API. In some cases you will see a much more appropriate pattern for your company/project and will be better just ignoraing all that is written here.