Routing

Interaction with your website or application will be done via URLs like /docs or /api/users. To make this work you need to define code that will be executed when a user visits these URLs. This is what routing is: you declare routes which associate a URL and a function, in the basic cases.

from expanse.routing.router import Router


def say_hello() -> str:
    return "Hello world!"


router = Router()
router.get("/hello", say_hello)

Using route declaration files

To make route declaration more easily discoverable and manageable, it is recommended to use route declaration files located in the routes directory. These files are standard Python files which need to expose a routes function that accepts a Router instance.

from expanse.routing.router import Router


def say_hello() -> str:
    return "Hello world!"


def routes(router: Router) -> None:
    router.get("/hello", say_hello)

By default, Expanse defines two route files: routes/web.py and routes/api.py. These files are automatically loaded by your application's RouteServiceProvider, located in the app/providers/route_service_provider.py file. You can remove them or add more as needed.

The app/routes/web.py file defines routes that are for your web interface. They are automatically assigned the web middleware group, which provides standard features like session management and CSRF protection.

The app/routes/api.py file defines stateless routes with the api middleware group.

Unless you use Expanse to implement only an API (in that case you can remove the app/routes/web.py file) you will start defining routes in the app/routes/web.py file. These routes are easily accessible in a web browser by entering the defined URL. For instance, the following route would be accessible by going to http://localhost:8000/articles:

from expanse.routing.router import Router


def list_articles() -> Response:
    ...


def routes(router: Router) -> None:
    router.get("/articles", list_articles)

Routes defined in the app/routes/api.py file are part of a route group. This group is assigned the /api URI prefix which will be automatically applied, so you do not need to manually apply it to every route in the file. You may modify the prefix and other route group options by modifying your RouteServiceProvider class.

Route handler

The route handler is responsible for processing the incoming request and returning a response or stopping the request by raising an exception.

The handler can be a simple function or a controller method:

from expanse.routing.router import Router

from app.http.controllers.article_controller import ArticleController


def routes(router: Router) -> None:
    router.get("/articles", ArticleController.index)

If the handler is a controller method like in the example above, Expanse will automatically create an instance of the controller class during the HTTP request using the service container and execute the corresponding method.

For more information about controllers, you can refer to the Controllers section.

If the controller has no dependencies or does not depend on scoped services to be instantiated, you can declare the controller as a singleton in the service container. This will improve performance by avoiding the overhead of creating a new instance of the controller for every request.

self._app.singleton(ArticleController)

Available methods

You can register routes that correspond to any HTTP verbs:

from expanse.routing.router import Router

router = Router()

router.get(uri, function)
router.post(uri, function)
router.put(uri, function)
router.patch(uri, function)
router.delete(uri, function)
router.options(uri, function)

Dependency injection

You may type-hint any dependencies required by your route in your route's function signature. The declared dependencies will automatically be resolved and injected into the function by the Expanse service container. For example, you may type-hint the expanse.http.request.Request class to have the current HTTP request automatically injected into your route function:

from expanse.http.request import Request


def say_hello(request: Request) -> str:
    return "Hello world!"

Route parameters

You can specify that parts of your routes URI is a parameter or variable that need to be captured and, optionally, validated. This is typically the case of retrieving the user's ID from the URL:

router.get("/users/{id}", lambda id: f"User {id}")
URL id
/users/1 1
/users/john-doe john-doe

As you can see, parameters are declared in the same way as Python's format strings, i.e. encased in {} braces.

A URI can specify multiple parameters if necessary, as long as they have unique names:

def get_post(id: int, post_id: int) -> str:
    return f"User {id}, post {post_id}"


router.get("/users/{id}/posts/{post_id}", get_post)
URL id post_id
/users/1/posts/4 1 4
/users/john-doe/posts/foo john-doe foo

Validating parameters

In addition to the name of you parameter, you can specify a validator that will be applied to the parameter:

router.get("/usesr/{id:int}", lambda id: f"User {id}")

Expanse supports natively five validators: float, int, path, str, and uuid.

Using type-hints in your route function signature is on the roadmap and will be available in a future version. This will make it easier to document your routes and make them more readable. This will allow things like this:

def get_user(id: int) -> str:
    return f"User {id}"

router.get("/users/{id}", get_user)

Named routes

You can optionally give a unique name to your routes. This is a convenient way to generate URLs to specific routes. To specify a name to a route, you can use the name keyword argument when declaring your route:

router.get("/user/{id}", lambda id: f"User {id}", name="users.retrieve")

Generating named routes URLs

Once routes have a dedicated name, you can easily generate the corresponding URL by using the route() method of the Router:

url = router.route("users.list")

Similarly, you can easily redirect to a named route with a Redirect instance:

from expanse.http.redirect import Redirect


def handler(redirect: Redirect) -> Response:
    return redirect.to_route("user.list")

If the named route has parameters, you can specify them as a dictionary:

router.route("users.retrieve", {"id": 1})
redirect.to_route("users.retrieve", {"id": 1})

Any additional parameters will be added to the query string of the generated URL:

router.route("users.retrieve", {"id": 1, "extended": "true"})

# /users/1?extended=true

Grouping routes

Route groups are a convenient way to organize your routes and share common elements like middleware or prefix between them.

Route groups are created via the group() method of the Router:

from expanse.routing.router import Router

router: Router
with router.group("api") as group:
    group.get("/users/{id}", get_user, name="users.retrieve")

Note that group names and route names are appended, so this example the route you have api.users.retrieve.

Prefixing routes in a group

The URI of routes pertaining to a group can be prefixed by using the prefix keyword argument:

from expanse.routing.router import Router

router: Router
with router.group("api", prefix="/api") as group:
    group.get("/users/{id}", get_user, name="retrieve")

In this example, the generate URL for the retrieve route would be /api/users/{id}.

Middleware

If you need to share a set of middleware to every route in a group you can use the middleware() method:

from expanse.routing.router import Router

router: Router
with router.group("users") as group:
    group.middleware(FirstMiddleware, SecondMiddleware)
    ...