Router
defines which middleware and Handler
will process an HTTP request. This is the most fundamental and core functionality in Salvo.
Internally, Router
consists of a series of filters. When a request arrives, the router tests itself and its descendants in the order they were added, from top to bottom, to see if they can match the request. If a match is successful, the middleware along the entire chain formed by the router and its descendants is executed sequentially. If during processing, the Response
status is set to an error (4XX, 5XX) or a redirect (3XX), subsequent middleware and Handler
will be skipped. You can also manually call ctrl.skip_rest()
to skip the remaining middleware and Handler
.
During the matching process, there is a URL path component that can be considered an object to be fully consumed by the filters. If all filters in a router succeed and the URL path component is completely consumed, it is considered a "successful match."
For example:
This is equivalent to:
If accessing GET /articles/
, the match is successful, and list_articles
is executed. However, if accessing GET /articles/123
, the route match fails and returns a 404 error because Router::with_path("articles")
only consumes the /articles
part of the URL path, leaving /123
unconsumed, resulting in a failed match. To make the match successful, the route can be modified as:
Here, {**}
matches any remaining path, so it can handle GET /articles/123
and execute list_articles
.
We can define routes in a flat style:
We can also define routes hierarchically, which is the recommended approach:
This form of definition makes the router structure clear and simple for complex projects.
Many methods in Router
return Self
to enable method chaining. Sometimes, you may need to conditionally determine how to route. The routing system also provides the then
function for easy use:
This example shows that routes for creating, editing, and deleting articles are only added when the server is in admin_mode
.
In the above code, {id}
defines a parameter. We can retrieve its value from the Request
instance:
{id}
matches a segment in the path. Normally, an article id
is just a number, so we can use a regular expression to restrict the matching rule, such as r"{id|\d+}"
.
For numeric types, a simpler method is to use <id:num>
with the following syntax:
{id:num}
: Matches any number of digits.{id:num[10]}
: Matches exactly 10 digits.{id:num(..10)}
: Matches 1 to 9 digits.{id:num(3..10)}
: Matches 3 to 9 digits.{id:num(..=10)}
: Matches 1 to 10 digits.{id:num(3..=10)}
: Matches 3 to 10 digits.{id:num(10..)}
: Matches at least 10 digits.You can also use {**}
, {*+}
, or {*?}
to match all remaining path segments. For better readability, you can add descriptive names to make the path semantics clearer, such as {**file_path}
.
{**}
: Matches any remaining path, including an empty string. For example, /files/{**rest_path}
matches /files
, /files/abc.txt
, and /files/dir/abc.txt
.{*+}
: Matches any remaining path but excludes empty strings. For example, /files/{*+rest_path}
does not match /files
but matches /files/abc.txt
and /files/dir/abc.txt
.{*?}
: Matches any remaining path but only a single segment. For example, /files/{*?rest_path}
does not match /files/dir/abc.txt
but matches /files
and /files/abc.txt
.Multiple expressions can be combined to match the same path segment, such as /articles/article_{id:num}/
or /images/{name}.{ext}
.
Middleware can be added using the hoop
function on a router:
In this example, the root router uses check_authed
to verify whether the current user is logged in. All descendant routers are affected by this middleware.
If users only need to browse writer information and articles without logging in, we can structure the routes as follows:
Even though two routers have the same path definition path("articles")
, they can still be added to the same parent router.
Router
internally uses filters to determine whether a route matches. Filters support basic logical operations like or
and and
. A router can contain multiple filters, and the route matches successfully only if all filters succeed.
A website's path structure is hierarchical, but this hierarchy does not necessarily align with the router's organizational structure. A single website path may correspond to multiple router nodes. For example, some content under the articles/
path may require login, while others do not. We can organize login-required subpaths under a router with authentication middleware and non-login-required subpaths under another router without it:
Routers use filters to match requests and forward them to the corresponding middleware and Handler
for processing.
path
and method
are the two most commonly used filters. path
matches path components, while method
matches HTTP request methods like GET, POST, and PATCH.
Filters can be combined using and
and or
:
Path-based filters are the most frequently used. Path filters can define parameters, such as:
In the Handler
, these parameters can be retrieved using the Request
object's get_param
function:
Method filters match requests based on HTTP methods, such as:
Here, get
, patch
, and delete
are method filters. This is equivalent to:
For frequently used matching patterns, we can assign a shorthand name using PathFilter::register_wisp_regex
or PathFilter::register_wisp_builder
. For example, GUID formats often appear in paths. Normally, we would write:
Writing such complex regex patterns repeatedly is error-prone and unaesthetic. Instead, we can do this:
After registering once, we can simply use {id:guid}
to match GUIDs, simplifying the code.
The main differences between routing-based web frameworks (like Salvo) and traditional MVC or Controller-based frameworks are:
Flexibility: Routing allows more flexible definition of request handling, enabling precise control over each path's logic. For example, in Salvo, you can directly define handlers for specific paths:
In Controller-based designs, you typically define a controller class with methods for different requests:
Middleware Integration: Routing frameworks often provide cleaner middleware integration, allowing middleware to be applied to specific routes. In Salvo, middleware can be precisely targeted:
Code Organization: Routing encourages organizing code by functionality or API endpoints rather than strict MVC layers.
For example:
Lightweight: Routing designs are typically more lightweight, reducing framework-imposed constraints. You only need to include necessary components without adhering to rigid structures.
Routing makes API development more intuitive, especially for modern microservices and RESTful APIs. In frameworks like Salvo, routing is the core concept, directly reflecting API structure and behavior, making code easier to understand and maintain. In contrast, traditional Controller designs often require more configuration and conventions for the same functionality.
Category | Method | Description |
---|---|---|
Creation/Access | new() | Creates a new router |
routers()/routers_mut() | Gets references/mutable references to child routers | |
hoops()/hoops_mut() | Gets references/mutable references to middleware | |
filters()/filters_mut() | Gets references/mutable references to filters | |
Routing Organization | unshift() | Inserts a child router at the beginning |
insert() | Inserts a child router at a specified position | |
push() | Adds a child router | |
append() | Adds multiple child routers | |
then() | Customizes router chain configuration | |
Middleware | with_hoop()/hoop() | Creates/adds middleware |
with_hoop_when()/hoop_when() | Creates/adds conditional middleware | |
Path Filtering | with_path()/path() | Creates/adds path filters |
with_filter()/filter() | Creates/adds filters | |
with_filter_fn()/filter_fn() | Creates/adds function-based filters | |
Network Filtering | scheme() | Adds protocol filters |
host()/with_host() | Adds/creates host filters | |
port()/with_port() | Adds/creates port filters | |
HTTP Methods | get()/post()/put() | Creates routes for corresponding HTTP methods |
delete()/patch()/head()/options() | Creates routes for corresponding HTTP methods | |
Handlers | goal() | Sets the route handler |
Matching Detection | detect() | Checks if the router matches a request |