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 "/":
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:
It is also possible to specify the expected type, using typing
annotations:
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:
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:
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
:
And then use it in routes:
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.
Defining a fallback route¶
To define a fallback route that handles web requests not handled by any other
route, use app.router.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:
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:
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:
And use it when registering routes:
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
# ...
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 uses 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.
Since version 2.3.0
, all routes in BlackSheep behave this way and decorators
in app.router
and app.router.controllers_routes
can be used
interchangeably.
Before version 2.3.0
, it is necessary to use the correct methods when
defining request handlers: the decorators of the router.controllers_routes
for controllers' methods, and the decorators of the router
for request
handlers defined using functions.
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.
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.
How to track routes that matched a request¶
BlackSheep by default does not track which route matched a web request, because this is not always necessary. However, for logging purposes it can be useful to log the route pattern instead of the exact request URL, to reduce logs cardinality.
One option to keep track of the route that matches a request is to wrap the
get_match
of the Application's router:
def wrap_get_route_match(
fn: Callable[[Request], Optional[RouteMatch]]
) -> Callable[[Request], Optional[RouteMatch]]:
@wraps(fn)
def get_route_match(request: Request) -> Optional[RouteMatch]:
match = fn(request)
request.route = match.pattern.decode() if match else "Not Found" # type: ignore
return match
return get_route_match
app.router.get_match = wrap_get_route_match(app.router.get_match) # type: ignore
If monkey-patching methods in Python looks ugly, a specific Router
class can
be used, like in the following example:
from blacksheep import Application, Router
from blacksheep.messages import Request
from blacksheep.server.routing import RouteMatch
class TrackingRouter(Router):
def get_match(self, request: Request) -> RouteMatch | None:
match = super().get_match(request)
request.route = match.pattern.decode() if match else "Not Found" # type: ignore
return match
app = Application(router=TrackingRouter())
@app.router.get("/*")
def home(request):
return (
f"Request path: {request.url.path.decode()}\n"
+ f"Request route path: {request.route}\n"
)
If attaching additional properties to the request object also looks suboptimal,
a WeakKeyDictionary
can be used to store additional information about the
request object, like in this example:
import weakref
from blacksheep import Application, Router
from blacksheep.messages import Request
from blacksheep.server.routing import RouteMatch
class TrackingRouter(Router):
def __init__(self):
super().__init__()
self.requests_routes = weakref.WeakKeyDictionary()
def get_match(self, request: Request) -> RouteMatch | None:
match = super().get_match(request)
self.requests_routes[request] = match.pattern.decode() if match else "Not Found"
return match
router = TrackingRouter()
app = Application(router=router)
@app.router.get("/*")
def home(request):
return (
f"Request path: {request.url.path.decode()}\n"
+ f"Request route path: {router.requests_routes[request]}\n"
)
Last modified on: 2025-05-11 19:57:17