Views: 35
0 0
Read Time:10 Minute, 9 Second

FastAPI is a modern, high-performance web framework for building APIs with Python 3.7+ based on standard Python type hints. It’s designed to create fast, reliable, and production-ready APIs with minimal effort, making it an excellent choice for developers looking to streamline backend development. FastAPI leverages asynchronous programming, making it highly efficient and capable of handling a large number of concurrent requests with minimal latency. Additionally, it includes built-in support for data validation, serialization, and documentation generation, making it both powerful and developer-friendly.

In FastAPI, routing plays a crucial role in defining how HTTP requests are handled and directed within an application. Basic routing is easy to set up, but advanced routing features allow developers to build more dynamic, complex, and customizable APIs. This blog post dives into the advanced routing capabilities of FastAPI.

Setup the environment

For the next steps we will work with

Create the Poetry project and install dependencies

First of all we have to create the poetry project, so for doing that, navigate to working folder and run this command

% poetry new advanced-routing
poetry new advanced-routing
Created package advanced_routing in advanced-routing

In your working directory now you should have a new folder advanced-routing, which structure should match the one below

advanced-routing
├── pyproject.toml
├── README.md
├── advanced-routing
│   └── __init__.py
└── tests
    └── __init__.py

Add FastAPI to our project

poetry add FastAPI
Creating virtualenv advanced-routing-csjMiuwX-py3.13 in /Users/brewedbrilliance/Library/Caches/pypoetry/virtualenvs
Using version ^0.115.4 for fastapi

Updating dependencies
Resolving dependencies... (1.5s)

Package operations: 9 installs, 0 updates, 0 removals

  - Installing idna (3.10)
  - Installing sniffio (1.3.1)
  - Installing typing-extensions (4.12.2)
  - Installing annotated-types (0.7.0)
  - Installing anyio (4.6.2.post1)
  - Installing pydantic-core (2.23.4)
  - Installing pydantic (2.9.2)
  - Installing starlette (0.41.2)
  - Installing fastapi (0.115.4)

Writing lock file

At this stage we have everything we need!

Note: make sure that you will run now the poetry shell command, so you can activate the virtual env

So let’s start with creating initially a standard FastAPI application

Create the basic FastAPI application

Let’s create our basic FastAPI application like explained here (https://fastapi.tiangolo.com/#installation)

Create a main.py file in the advanced-routing folder so that the directory structure would match this one below

advanced-routing
├── pyproject.toml
├── README.md
├── advanced-routing
│   └── __init__.py
│   └── main.py      # <=== new file
└── tests
    └── __init__.py

In main.py we will add just these lines

from typing import Union
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}


@app.get("/items/{item_id}")
def read_item(item_id: int):
    return {"item_id": item_id}

All we need to do now is to start our dev server with this application by doing from the command line

fastapi dev main.py --port 8085

This command will start our development server, with reload set to true, that means that the application will be constantly watched for changes and if a change is applied to any of the source file, the server will reload.

Screenshot-2024-11-10-alle-13.21.42 FastAPI advanced routing

If we want to check our results all we have to do is just to navigate with our browser to http://localhost:8085/ or http://localhost:8085/items/3

If we want to add more endpoints all we have to do, accordingly to the way FastAPI works, is to just add functions with the decorator we will want (@app.get, @app.post, @app.delete, @app.put)

Using the FastAPI Route

The example above is pretty basic, for bigger application we need to add more endpoints and is not plausible to have all of them defined in one file, for any application that is constantly evolving the maintenance will start to become really hard for an evergrowing list of endpoints. For solving this problem, we can leverage the Routes that FastAPI has and we can split our application into smaller indipendent parts. For this purpose we will have the items.py that will create the route for items endpoints, and the users.py for the users endpoints

We want that our directory structure to look like

advanced-routing
├── pyproject.toml
├── README.md
├── advanced-routing
│   └── __init__.py
│   └── main.py      
│   └── routers.           # <=== new folder
│   │   ├── __init__.py    # <=== new file
│   │   ├── items.py       # <=== new file
│   │   ├── users.py       # <=== new file
└── tests
    └── __init__.py

For the simplicity of this guide we will have 2 end points per file mostly identical

Our items.py will looks like

# items.py
from fastapi import APIRouter
from typing import Union


router = APIRouter()


@router.get("/items/{item_id}")
def read_item(item_id: int):
    return {
        "item_id": item_id, 
        "source": "items"
        }

Our users.py will looks like

from fastapi import APIRouter
from typing import Union


router = APIRouter()

@router.get("/users/{user_id}")
def read_user(user_id: int):
    return {
        "user_id": user_id, 
        "source": "users"
    }

Our main.py instead will be slightly changed

from fastapi import FastAPI
from .routes import items, users

app = FastAPI()
app.include_router(items.router)
app.include_router(users.router)

@app.get("/")
def read_root():
    return {"Hello": "World2"}

In the FastAPI documenation these are called submodules and more info can be found at this page

https://fastapi.tiangolo.com/tutorial/bigger-applications

So far, we’ve taken a simple approach: we split the application into submodules, each imported as an APIRouter instance and then registered with the main app. This setup works well, but it becomes challenging when you want to list, document, or inspect all the endpoints to understand expected requests and responses. What if we could load all the endpoints dynamically from a JSON file or dictionary? Wouldn’t that be great? Well, the APIRouter class provides a method just for this: add_api_route.”

FastAPI add_api_route

In the FastAPIRouter documentation page (https://fastapi.tiangolo.com/reference/apirouter/#fastapi.APIRouter.include_router–example) can be found (together with all the source code) the call to this method that is the essence of how the APIRouter class works. Around line 1300 you can see this interesting piece of code

self.add_api_route(
    prefix + route.path,
    route.endpoint,
    response_model=route.response_model,
    status_code=route.status_code,
    tags=current_tags,
    dependencies=current_dependencies,
    summary=route.summary,
    description=route.description,
    response_description=route.response_description,
    responses=combined_responses,
    deprecated=route.deprecated or deprecated or self.deprecated,
    methods=route.methods,
    operation_id=route.operation_id,
    response_model_include=route.response_model_include,
    response_model_exclude=route.response_model_exclude,
    response_model_by_alias=route.response_model_by_alias,
    response_model_exclude_unset=route.response_model_exclude_unset,
    response_model_exclude_defaults=route.response_model_exclude_defaults,
    response_model_exclude_none=route.response_model_exclude_none,
    include_in_schema=route.include_in_schema
    and self.include_in_schema
    and include_in_schema,
    response_class=use_response_class,
    name=route.name,
    route_class_override=type(route),
    callbacks=current_callbacks,
    openapi_extra=route.openapi_extra,
    generate_unique_id_function=current_generate_unique_id,
)

This lead to my curiosity and by looking at the source code of fastapi/routing.py you can see that as part of the class definition there is this method defined as

def add_api_route(
    self,
    path: str,
    endpoint: Callable[..., Any],
    *,
    response_model: Any = Default(None),
    status_code: Optional[int] = None,
    tags: Optional[List[Union[str, Enum]]] = None,
    dependencies: Optional[Sequence[params.Depends]] = None,
    summary: Optional[str] = None,
    description: Optional[str] = None,
    response_description: str = "Successful Response",
    responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
    deprecated: Optional[bool] = None,
    methods: Optional[Union[Set[str], List[str]]] = None,
    operation_id: Optional[str] = None,
    response_model_include: Optional[IncEx] = None,
    response_model_exclude: Optional[IncEx] = None,
    response_model_by_alias: bool = True,
    response_model_exclude_unset: bool = False,
    response_model_exclude_defaults: bool = False,
    response_model_exclude_none: bool = False,
    include_in_schema: bool = True,
    response_class: Union[Type[Response], DefaultPlaceholder] = Default(
        JSONResponse
    ),
    name: Optional[str] = None,
    route_class_override: Optional[Type[APIRoute]] = None,
    callbacks: Optional[List[BaseRoute]] = None,
    openapi_extra: Optional[Dict[str, Any]] = None,
    generate_unique_id_function: Union[
        Callable[[APIRoute], str], DefaultPlaceholder
    ] = Default(generate_unique_id),
) -> None:

Since this is a public method defined in the APIRouter class, we’re free to call it and register endpoints programmatically. For example, let’s look at items.py and users.py updated with this approach. This article won’t cover each parameter in detail; for now, we’ll focus only on the first parameter, which defines the URL path, and on the endpoint value, which must be a callable object (a function or a class method). In items.py (as users.py) we have one endpoint (/users/{id}) that can be registered as

# items.py
from fastapi import APIRouter
from typing import Union


def read_item(user_id: int):
    return {
        "item_id": user_id, 
        "source": "items"
    }


router = APIRouter()
router.add_api_route(
    "/item/{user_id}",
    endpoint=read_item,
    response_model={},
    methods=["GET"]
)
# users.py
from fastapi import APIRouter
from typing import Union


def read_user(user_id: int):
    return {
        "user_id": user_id, 
        "source": "users"
    }


router = APIRouter()
router.add_api_route(
    "/users/{user_id}",
    endpoint=read_user,
    response_model={},
    methods=["GET"]
)

Once saved, you’ll notice no changes in the application behavior; it will still serve the exact same endpoints and responses. To enhance this, we could declare a dictionary with all endpoints, associated methods, and callables, and loop through it for route bindings. This leads to the next version of our example: we’ll have a list of dictionaries in routes_configuration.py, where each entry defines a route. The submodules will contain only the callables, and our main module will simply import the dictionary and assign the routes.

Our routes configuration file will look like

Final Version

routes_configuration = [
    {
        "url": "/users/{user_id}",
        "callable": "read_user",
        "module": "users",
        "response_model": {},
        "methods": ["GET"]
    },
    {
        "url": "/items/{item_id}",
        "callable": "read_item",
        "module": "items",
        "response_model": {},
        "methods": ["GET"]
    },
]

Our items.py and users.py will be instead like

items.py
########## 
from typing import Union

def read_item(user_id: int):
    return {
        "item_id": user_id, 
        "source": "items"
    }

users.py
########## 
from typing import Union

def read_user(user_id: int):
    return {
        "user_id": user_id, 
        "source": "users"
    }

Our main.py instead will be slightly different because we have to import the modules, the APIRoute and bind the routes. After all the routes have been bound to the APIRouter instance we need to register that into the main router that is attached to the application itself so that can be publicly accessible

from fastapi import FastAPI, APIRouter
from .routes import items, users
from .routes.routes_configuration import routes_configuration
app = FastAPI()

router = APIRouter()


for route in routes_configuration:
    router.add_api_route(
        route.get("url"),
        endpoint=getattr(globals()["users"], 'read_user'),
        methods=route.get("methods"),
        response_model=route.get("response_model")
    )


app.router = router

@app.get("/")
def read_root():
    return {"Hello": "World2"}

And that’s it! In this small example, globals is being used; however, this is not the correct way to do it—it’s just to demonstrate the potential power of this technique. The best approach would be to create a class and then dynamically call its methods. We might cover this in one of the upcoming articles, but this is giving you the general idea of how ti could work, as long the routes_configuration will be there, all the endpoints will be created and nothing is blocking the fact that the endpoint list can be generated (for example) by looking at a database table.

I hope you enjoyed this guide and if so share and let us grow

buy_me_a_coffee-1 FastAPI advanced routing
Donate

cc4ae2d7d0e2367495d9f75c31beef8e?s=400&d=robohash&r=g FastAPI advanced routing

About Post Author

brewedbrilliance.net

Experienced software architect with a spasmodic passion for tech, software, programming. With more than 20years of experience I've decided to share all my knowledge in pills through this blog. Please feel free to contact me if there is any topic that you would love to cover
happy FastAPI advanced routing
Happy
0 %
sad FastAPI advanced routing
Sad
0 %
excited FastAPI advanced routing
Excited
0 %
sleepy FastAPI advanced routing
Sleepy
0 %
angry FastAPI advanced routing
Angry
0 %
surprise FastAPI advanced routing
Surprise
0 %

Share this content:

By brewedbrilliance.net

Experienced software architect with a spasmodic passion for tech, software, programming. With more than 20years of experience I've decided to share all my knowledge in pills through this blog. Please feel free to contact me if there is any topic that you would love to cover

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x