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 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.
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