A web application in Rust built with Tokio Axum and the Hyper server.
src/main.rs
: The main entry point of the application.assets
: static assets such as images, stylesheets, etc.templates
: templates for HTML pages. This is the default location for templates when using theaskama
crate.
Routing in Axum similar to routing in low-level HTTP libraries like Sinatra, Flask and Express.
See src/server.rs
for the routing.
It is worth to mention that the concept of route and nested routes.
A route is a single path, like /foo
or /bar
. A nested route is a the root route
and everything "under" it. For example, /root/x
and /root/y/z
are nested routes under /root
.
When declaring the routing, keep this in mind:
We serve a single file on a single (non-nested) route like this:
let router = axum::routing::Router::new()
.route_service("/assets/foo.html", ServeFile::new("assets/foo.html"));
We can serve a whole directory like this. Note that we use .nest_service
since it nests all the routes,
.route_service
would route the root path to the service only.
let router = axum::routing::Router::new()
.nest_service("/assets", ServeDir::new("assets"));
You can use path parameters to turn path segments into parameters to the handler function:
let router = axum::routing::Router::new()
.route("/languages/years/:year", get(languages_from_year));
The handler function would look like this, using the Path
extractor. It contains a
single value or a tuple if you match multiple path segments:
// Path is an Axum Extract to get the matched value from the path (see below in the route configuration)
async fn languages_from_year(Path(year): Path<u32>) -> LanguagesTemplate { /* ... */ }
The query parameters are extracted using the Query
extractor. It can be used to extract the
query parameters into an stringly typed HashMap of key-value pairs or a typed struct:
// Stringly typed
async fn languages_from_year_query(Query(params): Query<HashMap<String, String>>)
-> LanguagesTemplate { /* ... */ }
// Typed struct
/// Axum can use `serde` to deserialize the query parameters into a struct
#[derive(Deserialize)]
pub(crate) struct LanguagesFilter {
year_from_inclusive: Option<u32>,
year_to_exclusive: Option<u32>,
}
async fn languages_by_struct_query(filter: Query<LanguagesFilter>)
-> LanguagesTemplate { /* ... */ }
You can get some errors for missing fields and empty fields in the query string when it is deserialized with serde
.
See the serde documentation for #[serde(deserialize_with = ...)]
to see how to define the behaviour for these scenarios.
#[derive(Deserialize)]
pub(crate) struct LanguagesFilterThatAcceptsEmptyQueryParameterValuesAsNone {
#[serde(deserialize_with = "empty_string_as_none")]
year_from_inclusive: Option<u32>,
#[serde(deserialize_with = "empty_string_as_none")]
year_to_exclusive: Option<u32>,
}
From https://github.com/tokio-rs/axum/blob/main/examples/query-params-with-empty-strings/src/main.rs
/// Serde deserialization decorator to map empty Strings to None,
fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
T::Err: fmt::Display,
{
let opt = Option::<String>::deserialize(de)?;
match opt.as_deref() {
None | Some("") => Ok(None),
Some(s) => FromStr::from_str(s).map_err(de::Error::custom).map(Some),
}
}
You can use the try_from_uri
method of the Query
extractor to test the query parsing like this.
This is very useful when using bespoke deserialize_with
functions.
#[test]
fn languages_filter_can_deserialize_when_all_query_string_params_are_present_and_valid() {
let uri = Uri::builder().path_and_query("/?year_from_inclusive=1950&year_to_exclusive=1970").build().unwrap();
let q = Query::<LanguagesFilter>::try_from_uri(&uri).unwrap();
assert_eq!(Some(1950), q.year_from_inclusive);
assert_eq!(Some(1970), q.year_to_exclusive);
}
The error messages are terrible when the handler signatures are not correct.
Unfortunately Rust gives poor error messages if you try to use a function that doesn’t quite match what’s required by Handler.
https://docs.rs/axum/latest/axum/handler/index.html#debugging-handler-type-errors
Use axum-macros
crate and its debug_handler
macro to get better error messages.
Just apply it to the handler function:
#[debug_handler]
async fn foo( /* ... */ ) -> impl IntoResponse {
// ...
}
Tracing is enabled, see the use of the tracing macros like info!
.
See src/main.rs
for the configuration of the tracing library.
Note that it is not enough to configure the tracing library, in many cases
the libraries that are used also need to be configured to use tracing by enabling
the tracing
feature. See cargo.toml.
For example, for tower-http
the following is needed:
tower-http = { version = "0.4.3", features = ["fs", "trace"] }
The application uses the askama template library to render HTML pages.
It appears that askama
and tera
are popular choices for templating in Rust. However, askama
comes with axum
bindings, so we will use that for now.
We can use template inheritance to improve consistency and reduce duplication in our templates. See https://djc.github.io/askama/template_syntax.html
For example, see the /languages
route in src/server.rs
and the corresponding
templates in templates/languages
, e.g. templates/languages/base.html
and templates/languages/index.html
.
Published under the MIT License, see LICENSE.