Warning: Bocadillo is now UNMAINTAINED. Users are recommended to migrate to a supported alternative, such as Starlette or FastAPI. Please see #344 for more information.
Routing
Bocadillo makes it very easy to handle HTTP requests, i.e. mapping URL patterns to views.
Overview
In Bocadillo, routes map an URL pattern to a view, which can be a function or a class. When an application receives a request, its router invokes the view for the matching route which generates the response.
How are requests processed?
When an inbound HTTP request hits your Bocadillo application, the following algorithm is used to determine which view gets executed:
- Bocadillo runs through each URL pattern and stops at the first matching one, extracting the raw route parameters as well. If none can be found, an
HTTPError(404)
is raised. - Bocadillo checks that the matching route supports the requested HTTP method, and raises an
HTTPError(405)
exception if it does not. - When this is done, Bocadillo calls the view attached to the route. If any of the route parameters fails validation, an
HTTPError(400)
exception is raised. - If no pattern matches, or if an exception is raised in the process, Bocadillo invokes an appropriate error handler (see Route error handling below).
The router searches against the requested URL path. This does not include the domain name nor query parameters.
Advanced
About trailing slashesWhen no route matches the requested URL path, and the URL path does not contain a trailing slash, Bocadillo will add it and send a temporary redirect (302) response. The client will then automatically perform a new request, and the routing algorithm starts again.
For example, if /items
did not match any route, the client will be redirected to /items/
. If no route matches /items/
, an HTTPError(404)
exception is raised.
Please note that when this happen, CORS pre-flight requests will fail. For this reason, make sure to use a trailing slash when requesting your Bocadillo API from a web browser.
To disable this behavior, use the REDIRECT_TRAILING_SLASH
setting:
# settings.py
REDIRECT_TRAILING_SLASH = False
Routes examples
Here are a few example routes:
from bocadillo import App
app = App()
@app.route("/")
async def home(req, res):
pass
@app.route("/items/42")
async def get_item_42(req, res):
pass
@app.route("/items/{pk}")
async def get_items(req, res, pk: int):
pass
Note that:
- An URL pattern should start with a leading slash.
- Bocadillo honors the presence or absence of a trailing slash on the URL. It will not perform any redirection by default.
- Route parameters are passed as keyword arguments to the view.
Here's how a few example requests would be handled:
Requested URL path | Matched route | Reason |
---|---|---|
/ | home() | Only matching route. |
/items/13 | get_items() | Only matching route. |
/items/42 | get_item_42() | First route to match. |
/items/foo | None | get_items() requires pk to be an integer. |
/items/13/detail | None | No matching route*. |
* Route parameters search in individual URL parts. If you want to match entire sections of the URL, see Path-like route parameters.
Function-based views
A route maps an URL pattern to a view. A view consists in an asynchronous function that takes as input the request (req
by convention), the response (res
by convention), and any keyword arguments obtained from route or query parameters (more on this in the next sections).
We've already used function-based views in the examples above. One thing to remember is that views must be asynchronous, i.e. defined with the async def
syntax. This allows you to call arbitrary async code in views, e.g.:
import asyncio
async def find_post_content(slug: str) -> str:
await asyncio.sleep(1) # perhaps query a database here?
return "My awesome post"
@app.route("/post/{slug}")
async def retrieve_post(req, res, slug: str):
res.text = await find_post_content(slug)
Class-based views
Bocadillo also supports class-based views. Incoming requests get dispatched to the method on the class named after the requested HTTP method. For example, GET
is dispatched to .get()
, POST
is dispatched to .post()
, etc. Thanks to this mechanism, there is no base class — just write regular Python classes!
Here's an example route using class-based view:
@app.route("/")
class Home:
async def get(self, req, res):
res.text = 'Classes, oh my!'
async def post(self, req, res):
res.text = 'Roger that'
If the .handle()
method is implemented, all incoming requests are dispatched to it regardless of their HTTP method:
@app.route("/")
class Home:
async def handle(self, req, res):
res.text = 'Post it, get it, put it, delete it.'
IMPLEMENTATION DETAILS
Bocadillo actually has a View
base class, but you don't need to subclass it when building class-based views. It only exists as a unique representation to which function- and class-based views are internally converted to.
HTTP methods
Which HTTP methods are exposed on a route depends on the view function or class.
On class-based views, HTTP methods are exposed based on the class' methods. For example, the POST method is accepted if and only if the view implements .post()
.
On function-based views, you can use the case-insensitive methods
argument to @.route()
:
@app.route("/items", methods=["post"])
async def create_item(req, res):
# ...
res.status_code = 201
If methods
is not given, only safe HTTP methods are exposed, i.e. GET
and HEAD
.
When a non-allowed HTTP method is requested by a client, a 405 Not Allowed
error response is automatically returned.
Bocadillo automatically implements the HEAD
method if your route supports GET
.
We do this to allow URL checkers and web crawlers to examine endpoints without transferring the full request body.
Route parameters
Route parameters allow a single URL pattern to match a variety of URLs.
A route parameter is defined with the {param_name}
syntax. When a request is made to a matching URL, the parameter value is extracted and passed to the view as a keyword argument.
Consider the following route:
@app.route("/say/{message}")
async def say(req, res, message):
res.text = f"You said: '{message}'"
If a request is made to /say/hello
, the view will be given a keyword argument message
with the value "hello"
:
curl "http://localhost:8000/say/hello"
HTTP/1.1 200 OK
date: Sat, 18 May 2019 09:35:51 GMT
server: uvicorn
content-type: text/plain
content-length: 5
You said: 'hello'
Validation and conversion
Bocadillo provides a lightweight route parameter validation mechanism based on type annotations and the TypeSystem data validation library.
How it works: if the route parameter declared on the view has a type annotation, Bocadillo automatically converts the parameter value to that type. If it is not annotated, the value is passed as a string.
For example:
@app.route("/items/{pk}")
async def get_item(req, res, pk: int):
pass
By annotating pk
as an int
, Bocadillo automatically converts the extracted pk
parameter to an integer before passing it to the view. It's that simple!
If a parameter cannot be converted, a 400 Bad Request
response is returned with an explicit error message. For example:
curl "http://localhost:8000/items/notanumber"
{
"error": "400 Bad Request",
"status": 400,
"detail": {
"pk": "Must be a number."
}
}
You can also annotate parameters with a TypeSystem field directly:
from typesystem import Integer
@app.route("/items/{quantity}")
async def get_item(req, res, quantity: Integer(minimum=0)):
pass
As it turns out, annotating parameters with builtins such as int
or bool
only works because Bocadillo provides aliases to TypeSystem fields. Here's the full list of available aliases:
Type annotation | TypeSystem field |
---|---|
int | Integer() |
float | Float() |
bool | Boolean() |
decimal.Decimal | Decimal() |
datetime.datetime | DateTime() |
datetime.date | Date() |
datetime.time | Time() |
See also TypeSystem fields for the complete list of fields and their options.
Query parameters
Similarly to route parameters, query parameters present in the URL's querystring can be validated and injected into the view.
You can do so by declaring a parameter with a default value, and optionally type-annotating it:
ITEMS = list(range(10))
@app.route("/items")
async def get_items(req, res, limit: int = None):
res.json = ITEMS[:limit] if limit is not None else ITEMS[:]
Here's what the value of limit
will be for various requested URL paths:
Requested path | limit |
---|---|
/items | None |
/items?limit=5 | 5 (the integer) |
Validation works as expected: requesting /items?limit=notanumber
will result in a 400 Bad Request
error response.
{
"error": "400 Bad Request",
"status": 400,
"detail": {
"limit": "Must be a number."
}
}
NOTE
Raw query parameters can be accessed through the Request object.
Wildcard matching
Wildcard URL matching is made possible thanks the anonymous parameter {}
. This is useful to implement routes that serve as defaults when no other routes match the requested URL path.
For example, here's how to implement a "catch-all" route:
@app.route("{}")
async def hello(req, res):
res.text = "Hello, world!"
As you can see, the value of an anonymous parameter is not passed to the view. If you need access to the value, you should use a regular named route parameter.
CAVEATS
- Order matters. If
/foo/{}
is defined before/foo/bar
, making a request to/foo/bar
will match/foo/{}
. As a rule of thumb, define wildcard routes last. - The anonymous parameter
{}
expects a non-empty string. This means that, unlike the catch-all{}
(which is actually a special case), the pattern/{}
will not match the root URL/
because it expects a non-empty value after the leading slash. - Wildcard routes should not be used to implement 404 pages — Bocadillo already returns those for you.
Path-like route parameters
If you want a route parameter to match an entire section of the URL, use the :path
converter.
@app.route("/images/{location:path}")
async def get_image(req, res, location: str):
pass
Here, requesting /images/news/header.png
will result in the view being given location="news/header.png"
.
Redirecting
Inside a view, you can redirect to another URL by raising a Redirect
exception. The given URL can be internal (a path relative to the server's host) or external (an absolute URL).
from bocadillo import Redirect
@app.route("/")
async def index(req, res):
raise Redirect("/home")
# or, equivalently:
raise Redirect("http://localhost:8000/home")
Redirections are temporary (302) by default. To return a permanent (301) redirection, pass permanent=True
:
raise Redirect("/home", permanent=True)