This article introduces an approach to structure FastAPI applications with multiple services in mind. The proposed structure decomposes the individual services into packages and modules, following principles of abstraction and separation of concerns. The code discussed below can also be studied in its entirety in a dedicated companion GitHub repository.
FastAPI – Building High-performing Python APIs
FastAPI is a fast, highly intuitive and well-documented API framework based on Starlette. Despite being relatively new, it's gaining strong adoption by the developer community – it is even already being adopted in production by corporates like Netflix and Microsoft.
Following the UNIX philosophy of "doing one thing, and doing it well", separating parts of the application according to their task improves code readability and maintainability; ultimately reducing complexity. The main benefits of structuring applications in this way:
Separation of concerns – Decomposing the application into modules performing a single job. This allows accepting requests (top down: controller → service → data access) and returning responses (bottom up: data access → service → controller) with a clear separation of what particular functionality should be implemented in which particular module, reducing cognitive load for development.
Abstraction — Components of the application are designed in a reusable way. For instance,
ServiceResult is implemented as a generic outcome of a service operation (which may be successful and return a response, or unsuccessful and raise an exception) able to be used by all services of the app, keeping code DRY.
The granular nature of namespacing allow to distinguish parts of the application, e.g., routes versus business logic belonging to a particular service – grouping similar tasks together, while keeping distinct parts separated. Four principal packages are needed. For each service, one module is added to these four packages. For instance, a service called "Foo" requires the following modules (discussed in detail below):
./routers/ foo.py # Router instance and routes ./services/ foo.py # Business logic (including CRUD helpers) ./schemas/ foo.py # Data "schemas" (e.g., Pydantic models) ./models/ foo.py # Database models (e.g., SQLAlchemy models)
The four principal packages are complemented by two generic packages which contain application-specific (and not service-specific) functionality, such as configuration or utility functions.
Controller Layer – Routes
main, the application is instantiated and all routers are included. Additionally, middlewares and/or exception handlers are implemented. The example application discussed below is based on a service called
Foo, which requires a number of routes. To handle custom exceptions occurring at the service layer, as instances of class
AppExceptionCase, a respective exception handler is added to the application.