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