FastAPI Structuring

Posted on Sun 11 September 2022

When it comes to API design, structuring it is probably one the most fun things. Decomposition of the associated functions into the relevant prefixes etc. is not just fun but highly recommended for a clean implementation. The more sound the structure is, the more obvious and logical the interpretation of the API would be. This article will refer to its predecessor; FastAPI Testing.

As usual, for the sake of simplicity, we shall look at a simple API with the following three endpoints;

    root ('/') - for the welcome a default
    r1 ('/r1') - for route #1
    r2 ('/r2') - for route #2

As per the granular nature of our iteration, It is better to structure the project in a much more consistent way. So;

prj/
    main.py - the main entry point for the app
    api.py  - the aggregation of all routes
    routes/ - the core functionality divided into various files as a sub-module
        __init__.py - module init
        welcome.py  - a simple welcome
        ...

The directory structure, for the moment serves a clean enough structure. The description of what each file or directly is explained inline.

For better understanding we'll look at the code in reverse order. Bottom-up if you may. That brings us to the core functionality of welcome.py;

from fastapi import APIRouter

router = APIRouter()

@router.get('/')
def get():
    return {'msg': 'Welcome!'}

Compared to the preceding article one can see that it is pretty much the same, just the core functionality has gone under another APIRouter object and placed under routes/, for clarity. Do not worry it will start making sense when we add more routes.

Working with a single route we now move upwards to the aggregation stage of the API (even though there is only a single endpoint at the moment). The aggregation is implemented under api.py

from fastapi import APIRouter
from routes import welcome

router = APIRouter()

router.include_router(welcome.router, prefix='', tags=['welcome'])

To summarise, for the API aggregation api.py aggregates any API routes implemented under routes/ and at the moment there is only the welcome.py route which serves as the index.

A level up and we have the application main in main.py for this FastAPI. This looks as such;

from fastapi import FastAPI

import api

app = FastAPI()
app.include_router(api.router)

Now, this can be more complicated or can stay pretty much the same with complications beings trickled down to the lower levels like the API aggregator and ideally the core routes. If everything is in order, one should be able to run the app with something like uvicorn as;

    >uvicorn main:app

With the default serving parameters, the API should now be hosted at: http://127.0.0.1:8000/ and the default/index should produce a JSON response of "Welcome!".

Extension is a simple process again we are going to start at the core i.e routes. We are now going to add two addition routes r1.py and r2.py respectively. These look like;

from fastapi import APIRouter

router = APIRouter()

@router.get('/')
def get():
    return {'msg': 'This is route #1.'}

... and ...

from fastapi import APIRouter

router = APIRouter()

@router.get('/')
def get():
    return {'msg': 'This is route #2.'}

The keen observer will notice that, be it for the sake of simplicity, structurally, these are not very different than welcome.py. However, things will make sense once we go a level higher i.e. from routes/ to api.py;

from fastapi import APIRouter
from routes import welcome

# new routes
from routes import r1
from routes import r2

router = APIRouter()

router.include_router(welcome.router, prefix='', tags=['welcome'])

# new routes
router.include_router(r1.router, prefix='/r1', tags=['route1'])
router.include_router(r2.router, prefix='/r2', tags=['route2'])

One should be able to notice the subtle difference where we have added the two additional routes. The only difference is the prefixes for r1.py we have a prefix /r1 and for r2.py we have prefix /r2. Note the preceding '/' as that is needed. That should route the endpoints to the relevant functionality. So where do we stand? What is our complete API with all three endpoints? Well once we run the application as mentioned above we'll have an API serving the following endpoints:

http://127.0.0.1:8000/      - the default/index
http://127.0.0.1:8000/r1    - Route #1
http://127.0.0.1:8000/r2    - Route #2

Hopefully this article, gives one a glimpse into a relatively clean implementation of code for their API. Of course, for real projects One would expect that the routes contain some significant implementation.

The source code used in this article can be found HERE.

Go forth and prosper!