Skip to content

Routing

Server side routing refers to the ability of a web application to handle web requests using different functions, depending on the URL path and HTTP method. Each BlackSheep application is bound to a router, which provides several ways to define routes. A function that is bound to a route is called a "request handler", since its responsibility is to handle web requests and produce responses.

This page describes:

  • How to define request handlers.
  • How to use route parameters.
  • How to define a catch-all route.
  • How to define a fallback route.
  • How to use sub-routers and filters.
  • How to use the default router and other routers.

Defining request handlers

A request handler is a function used to produce responses. To become request handlers, functions must be bound to a route, that represents a certain URL path pattern. The Router class provides several methods to define request handlers: with decorators (🗡️ in the table below) and without decorators (🛡️):

Router method HTTP method Type
head HEAD 🗡️
get GET 🗡️
post POST 🗡️
put PUT 🗡️
delete DELETE 🗡️
trace TRACE 🗡️
options OPTIONS 🗡️
connect CONNECT 🗡️
patch PATCH 🗡️
add_head HEAD 🛡️
add_get GET 🛡️
add_post POST 🛡️
add_put PUT 🛡️
add_delete DELETE 🛡️
add_trace TRACE 🛡️
add_options OPTIONS 🛡️
add_connect CONNECT 🛡️
add_patch PATCH 🛡️

With decorators

The following example shows how to define a request handler for the root path of a web application "/":

from blacksheep import get


@get("/")
def hello_world():
    return "Hello World"

Alternatively, the application router offers a route method:

from blacksheep import route


@route("/example", methods=["GET", "HEAD", "TRACE"])
async def example():
    # HTTP GET /example
    # HTTP HEAD /example
    # HTTP TRACE /example
    return "Hello, World!"

Without decorators

Request handlers can be registered without decorators:

def hello_world():
    return "Hello World"


app.router.add_get("/", hello_world)
app.router.add_options("/", hello_world)

Request handlers as class methods

Request handlers can also be configured as class methods, defining classes that inherit the blacksheep.server.controllers.Controller class:

from dataclasses import dataclass

from blacksheep import Application, text, json
from blacksheep.server.controllers import Controller, get, post


app = Application()


# example input contract:
@dataclass
class CreateFooInput:
    name: str
    nice: bool


class Home(Controller):

    def greet(self):
        return "Hello World"

    @get("/")
    async def index(self):
        # HTTP GET /
        return text(self.greet())

    @get("/foo")
    async def foo(self):
        # HTTP GET /foo
        return json({"id": 1, "name": "foo", "nice": True})

    @post("/foo")
    async def create_foo(self, foo: CreateFooInput):
        # HTTP POST /foo
        # with foo instance automatically injected parsing
        # the request body as JSON
        # if the value cannot be parsed as CreateFooInput,
        # Bad Request is returned automatically
        return json({"status": True})

Route parameters

BlackSheep supports three ways to define route parameters:

  • "/:example" - using a single colon after a slash
  • "/{example}" - using curly braces
  • "/<example>" - using angle brackets (i.e. Flask notation)

Route parameters can be read from request.route_values, or bound automatically by the request handler's signature:

from blacksheep import get

@get("/api/cats/{cat_id}")
def get_cat(cat_id):
    # cat_id is bound automatically
    ...
from blacksheep import Request, get

@get("/{example}")
def handler(request: Request):
    # reading route values from the request object:
    value = request.route_values["example"]
    ...

It is also possible to specify the expected type, using typing annotations:

@get("/api/cats/{cat_id}")
def get_cat(cat_id: int):
    ...
from uuid import UUID


@get("/api/cats/{cat_id}")
def get_cat(cat_id: UUID):
    ...

In this case, BlackSheep will automatically produce an HTTP 400 Bad Request response if the input cannot be parsed into the expected type, producing a response body similar to this one:

Bad Request: Invalid value ['asdas'] for parameter `cat_id`; expected a valid
UUID.

Value patterns

By default, route parameters are matched by any string until the next slash "/" character. Having the following route:

@get("/api/movies/{movie_id}/actors/{actor_id}")
def get_movie_actor_details(movie_id: str, actor_id: str):
    ...

HTTP GET requests having the following paths are all matched:

/api/movies/12345/actors/200587

/api/movies/Trading-Places/actors/Denholm-Elliott

/api/movies/b5317165-ad31-47e2-8a2d-42dba8619b31/actors/a601d8f2-a1ab-4f20-aebf-60eda8e89df0

However, the framework supports more granular control on the expected value pattern. For example, to specify that movie_id and actor_id must be integers, it is possible to define route parameters this way:

"/api/movies/{int:movie_id}/actors/{int:actor_id}"

Warning

Value patterns only affect the regular expression used to match requests' URLs. They don't affect the type of the parameter after a web request is matched. Use type annotations as described above to enforce types of the variables as they are passed to the request handler.

The following value patterns are built-in:

Value pattern Description
str Any value that doesn't contain a slash "/".
int Any value that contains only numeric characters.
float Any value that contains only numeric characters and eventually a dot with digits.
path Any value to the end of the path.
uuid Any value that matches the UUID value pattern.

To define custom value patterns, extend the Route.value_patterns dictionary. The key of the dictionary is the name used by the parameter, while the value is a regular expression used to match the parameter's fragment. For example, to define a custom value pattern for route parameters composed of exactly two letters between a-z and A-Z:

Route.value_patterns["example"] = r"[a-zA-Z]{2}"

And then use it in routes:

"/{example:foo}"

Catch-all routes

To define a catch-all route that will match every request, use a route parameter with a path value pattern, like:

  • {path:name}, or <path:name>
from blacksheep import text


@get("/catch-all/{path:sub_path}")
def catch_all(sub_path: str):
    return text(sub_path)

For example, a request at /catch-all/anything/really.js would be matched by the route above, and the sub_path value would be anything/really.js.

It is also possible to define a catch-all route using a star sign *. To read the portion of the path caught by the star sign from the request object, read the "tail" property of request.route_values. But in this case the value of the caught path can only be read from the request object.

@get("/catch-all/*")
def catch_all(request):
    sub_path = request.route_values["tail"]

Defining a fallback route

To define a fallback route that handles web requests not handled by any other route, use app.router.fallback:

def fallback():
    return "OOPS! Nothing was found here!"


app.router.fallback = fallback

Using sub-routers and filters

The Router class supports filters for routes and sub-routers. In the following example, a web request for the root of the service "/" having a request header "X-Area" == "Test" gets the reply of the test_home request handler, and without such header the reply of the home request handler is returned.

from blacksheep import Application, Router


test_router = Router(headers={"X-Area": "Test"})

router = Router(sub_routers=[test_router])

@router.get("/")
def home():
    return "Home 1"

@test_router.get("/")
def test_home():
    return "Home 2"


app = Application(router=router)

A router can have filters based on headers, host name, query string parameters, and custom user-defined filters.

Query string filters can be defined using the params parameter, and host name filters can be defined using the host parameter:

filter_by_query = Router(params={"version": "1"})

filter_by_host  = Router(host="neoteroi.xyz")

Custom filters

To define a custom filter, define a type of RouteFilter and set it using the filters parameter:

from blacksheep import Application, Request, Router
from blacksheep.server.routing import RouteFilter


class CustomFilter(RouteFilter):

    def handle(self, request: Request) -> bool:
        # implement here the desired logic
        return True


example_router = Router(filters=[CustomFilter()])

Using the default router and other routers

The examples in the documentation show how to register routes using methods imported from the BlackSheep package:

from blacksheep import get

@get("")
async def home():
    ...

Or, for controllers:

from blacksheep.server.controllers import Controller, get


class Home(Controller):

    @get("/")
    async def index(self):
        ...

In this case, routes are registered using default singleton routers, used if an application is instantiated without specifying a router:

from blacksheep import Application


# This application uses the default singleton routers exposed by BlackSheep:
app = Application()

This works in most scenarios, when a single Application instance per process is used. For more complex scenarios, it is possible to instantiate a router and use it as desired:

# app/router.py

from blacksheep import Router


router = Router()

And use it when registering routes:

from app.router import router


@router.get("/")
async def home():
    ...

It is also possible to expose the router methods to reduce code verbosity, like the BlackSheep package does:

# app/router.py

from blacksheep import Router


router = Router()


get = router.get
post = router.post

# ...
from app.router import get


@get("/")
async def home():
    ...

Then specify the router when instantiating the application:

from blacksheep import Application

from app.router import router


# This application uses the router instantiated in app.router:
app = Application(router=router)

Controllers dedicated router

Controllers need a different kind of router, an instance of blacksheep.server.routing.RoutesRegistry. If using a dedicated router for controllers is desired, do this instead:

# app/controllers.py

from blacksheep import RoutesRegistry


controllers_router = RoutesRegistry()


get = controllers_router.get
post = controllers_router.post

# ...

Then when defining your controllers:

from blacksheep.server.controllers import Controller

from app.controllers import get, post


class Home(Controller):

    @get("/")
    async def index(self):
        ...
from blacksheep import Application

from app.controllers import controllers_router


# This application uses the controllers' router instantiated in app.controllers:
app = Application()
app.controllers_router = controllers_router

About Router and RoutesRegistry

Controller routes use a "RoutesRegistry" to support the dynamic generation of paths by controller class name. Controller routes are evaluated and merged into Application.router when the application starts.

Routing prefix

In some scenarios, it may be necessary to centrally manage prefixes for all request handlers. To set a prefix for all routes in a Router, use the prefix parameter in its constructor.

router = Router(prefix="/foo")

To globally configure a prefix for all routes, use the environment variable APP_ROUTE_PREFIX and specify the desired prefix as its value.

This feature is intended for applications deployed behind proxies. For more information, refer to Behind proxies.

Last modified on: 2025-04-22 08:29:25

RP
EW
RV
T