python-3.xrestpython-decoratorsaws-lambda-powertools

How to split python-decorated API router methods in different files


I cannot find a clean way to structure a python API project that splits the routes of a module (/invoices for this example) into different files, when using a decorator approach for the routing strategy.

The problem is the following:

api.py:

from invoices.adapters.api.routes import invoices_routes

app = APIGatewayRestResolver(enable_validation=True)
app.include_router(invoices_routes.invoices_router, prefix="/invoices")

Each Router is defined as a variable in a separate file that holds all methods linked to this router, which is as follows:

invoices_routes.py:

invoices_router = Router()

@invoices_router.post(
    "/",
)
def handle_post_invoice(...)

The issue is that, as the methods under prefix /invoices grow, the file that holds all methods will grow too, so it would be necessary to split the methods into multiple files, one per method.

I have tried a few solutions, but I dont like any of them, because the code gets more dirty:

Option 1 (does not work): just import the methods in the router file

I move the methods to separate files, and keep only the Router instantiation in the routes file. It does not work, because in order for the methods to be loaded in the router, their files have to imported, and thus, loaded.

Importing the file in the routes file (to solve previous problem) doesnt help, because I end up having circular dependencies:

invoices_routes.py:

from .get_invoice import handle_get_invoice # fix to load the method in the router, causes circular dep
invoices_router = Router()

get_invoice.py:

from invoices.adapters.api.routes.invoices_routes import (
    invoices_router # circular dep
)
@invoices_router.get("/<invoice_id>", tags=["Invoices"])
def handle_get_invoice(...)

Note: Im using aws-lambda-powertools, but this problem happens if you implement a simple router decorator that works in a similar way, adding the routing function to a Router when importing the function, properly decorated.

Option 2: keep the functions without decorators, and manually call the router decorators in the routes file

I dont like this one, because the code gets very ugly, and it separates logic that should be close:

invoices_routes.py:

from .get_invoice import handle_get_invoice

invoices_router = Router()

decorated_handle_get_invoice = router.post(
    "/",
)(handle_get_invoice)

get_invoice.py:

def handle_get_invoice(...)

Option 3: define a loading function in the routes file, which imports locally the decorated methods

This one is a bit cleaner, but still not that much. The method file keeps the same, but we need to alter the routes, and the API entrypoint:

invoices_routes.py:

invoices_router = Router()

def load_routes(app):
    # Note the local import to avoid circular deps
    from .get_invoice import handle_get_invoice
    app.include_router(invoices_router, prefix="/invoices")

api.py:

from invoices.adapters.api.routes import invoices_routes

app = APIGatewayRestResolver(enable_validation=True)
invoices_routes.load_routes(app)

I havent find a cleaner way to do it, but Im sure there has to be some standard, better way to do this, given that implementing an API with separated route files is very common.


Solution

  • You can make the code that creates a Router instance a separate module (named invoices_router.py in the example below) so that all modules that need the Router instance can import the module without a reverse dependency (the long paths in your example are omitted for brevity):

    invoices_router.py

    invoices_router = Router()
    

    get_invoice.py

    from invoices_router import invoices_router
    
    @invoices_router.get("/<invoice_id>", tags=["Invoices"])
    def handle_get_invoice(...): ...
    

    invoices_routes.py

    import get_invoice
    from invoices_router import invoices_router
    

    api.py

    from invoices_routes import invoices_router
    
    app = APIGatewayRestResolver(enable_validation=True)
    app.include_router(invoices_router, prefix="/invoices")