Routes and URL design

The point of a web framework is to make it easy to work with HTTP requests, which means managing routes and designing URLs. This guide shows you how to do this in Bocadillo.

Overview

In Bocadillo, routes map an URL pattern to a view function, class or method. When Bocadillo receives a request, the application router invokes the view of the matching route and returns a response.

How are requests processed?

When an inbound HTTP requests hits your Bocadillo application, the following algorithm is used to determine which view gets executed:

  1. Bocadillo runs through each URL pattern and stops at the first matching one, extracting the route parameters as well. If none can be found or any of the route parameters fails validation, an HTTPError(404) exception is raised.
  2. Bocadillo checks that the matching route supports the requested HTTP method and raises an HTTPError(405) exception if it does not.
  3. When this is done, Bocadillo calls the view attached to the route.
  4. If no pattern matches, or if an exception is raised in the process, Bocadillo invokes an appropriate error handler (see Route error handling below).

Examples

Here are a few example routes:

from bocadillo import App

app = App()

@app.route('/')
async def index(req, res):
    pass

@app.route('/listings/143/')
async def get_listing_143(req, res):
    pass

@app.route('/listings/{id:d}/')
async def get_listing(req, res, id: int):
    pass

Note that:

  • An URL pattern should start with a leading slash. If it doesn't, Bocadillo adds one for you (except for the catch-all pattern {}).
  • Bocadillo honors the presence or absence of a trailing slash on the URL. It will not perform any redirection by default.
  • Route parameters are defined using the F-string notation.
  • Route parameters can optionally use format specifiers to perform validation and conversion. For instance, in get_listing(), {id:d} validates that id is an integer. By default, route parameters are extracted as strings.

Here's how a few example requests would be handled:

  • A request to / would match the index() view.
  • /listings/143 would not match get_listing_143() because the latter's URL pattern includes a trailing slash.
  • /listings/foo/ would not match get_listing() because the latter's URL pattern requires that id be an integer.

What the router searches against

The router searches against the requested URL path — which does not include the domain name nor GET or POST parameters.

Route parameters

Route parameters allows a single URL pattern to match a variety of URLs. Their syntax is inspired by F-strings and is powered by parse.

Basic usage

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 made available to the view.

For example, consider the following route pattern:

"/say/{message}"

If a request is made to /say/hello, the view will be given a keyword argument message with the value "hello".

You can learn more about the format syntax in the parse documentation: Format Syntax.

Validation and conversion

Lightweight validation and conversion of route parameters can be achieved by decorating them with format specifiers.

This allows you to make sure the provided parameters comply with a certain format. When they don't, matching is considered to have failed.

The basic syntax is {param_name:specifier}. Common format specifiers include d for integers and w for alphanumerical characters.

A typical use case is using the d specifier to require that an id is an integer:

from bocadillo import App

app = App()

@app.route("/items/{id:d}")
async def get_item(req, res, id: int):
    pass

As you can tell from the type annotation, the id will be given as an integer to the view instead of a string thanks to the integer specifier d.

You can find the full list of supported specifiers in the parse documentation: Format Specification.

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 route matches the requested URL.

For example, the following snippet shows how to implement a "catch-all" route.

from bocadillo import App

app = App()

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

CAUTION

  • Order matters. If /foo/{} is defined before /foo/bar, making a request to /foo/bar will match the former.
  • The anonymous parameter {} expects a non-empty string. This means that, unlike the catch-all {}, 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 — see the next section for how Bocadillo deals with URLs that don't match any route.

Route error handling

When Bocadillo cannot find a matching route for the requested URL, a 404 Not Found error response is returned.

If a route was found but it did not support the requested HTTP method (e.g. POST or DELETE when only GET is supported), a 405 Method Not Allowed error response is returned.

See customizing error handling for how to customize this behavior.

Naming routes

Working with absolute URLs can quickly become impractical, as changes to a route's URL pattern may require changes across the whole code base.

To overcome this, all routes are given a name which can be referenced later.

Inferred route names

Bocadillo will assign a name to routes based on the name of their view function or class.

The inferred route name is always snake_cased, as shown in the table below.

View declaration Inferred route name
async def do_stuff(req, res): (or def do_stuff(req, res):) "do_stuff"
class DoStuff: "do_stuff"

Explicit route names

If you wish to specify an explicit name, use the name parameter to @app.route():

@app.route('/about/{who}', name='about_page')
async def about(req, res, who):
    res.html = f'<h1>About {who}</h1>'

Namespacing routes

You can specify a namespace in order to group route names together:

@app.route('/blog/', namespace='blog')
async def home(req, res):
    pass

The namespace will be prepended to the route's name (either inferred or explicit) and separated by a colon, e.g. resulting in blog:home for the above example.

TIP

If you find yourself namespacing a lot of routes under a common path prefix (like above), you might benefit from writing a recipe.

Reversing named routes

To get back the full URL path to a named route (including its optional namespace), use app.url_for(), passing required route parameters as keyword arguments:

>>> app.url_for('about', who='them')
'/about/them'
>>> app.url_for('blog:home')
'/blog/'

In templates, you can use the url_for() template global:

<h1>Hello, Bocadillo!</h1>
<p>
  <a href="{{ url_for('about', who='me') }}">About me</a>
</p>

WARNING

Referencing a non-existing named route with url_for() will trigger an HTTPError(404) exception — even in templates.

Specifying HTTP methods

Which HTTP methods are exposed on a route is managed at the view level.

On class-based views, HTTP methods are exposed according to the declared handlers. For example, the POST method is accepted if and only if the view defines .post().

On function-based views, you can use the @view() decorator and its case-insensitive methods argument. If methods is not given or the decorator is omitted altogether, only safe HTTP methods are exposed, i.e. GET and HEAD.

from bocadillo import view

@app.route("/posts/{pk}")
@view(methods=["delete"])
async def delete_blog_post(req, res, pk):
    res.status_code = 204

How are unsupported methods handled?

When a non-allowed HTTP method is used by a client, a 405 Not Allowed error response is automatically returned. When this happens, hooks will not be called either but HTTP middleware will.

Automatic implementation of HEAD

The HEAD HTTP method is used by systems such as URL checkers and web crawlers to examine a read-only resource without transferring the full request body.

As a result, Bocadillo implements the HEAD method automatically if your route supports GET.

Last Updated: 4/17/2019, 6:48:55 PM