Tomáš Jašek
April 16, 2024
Show that Rust is ready for web server development
…and many more on crates.io: Web Programming/HTTP Server
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(hello_world));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn hello_world() -> &'static str {
"Hello, World!"
}
Re-state that sending a request to the server causes the
hello_world
function to be called.
Problem: I have a web service that serves multiple endpoints. I’d like to invoke different functions depending on the URL.
Problem: In a single route handler, the caller has passed a request parameter. How can I access it?
let app = Router::new()
.route("/path/:user_id", get(get_user_by_id))
.route("/search", post(search))
.route("/form", post(process_form));
async fn get_user_by_id(Path(x): Path<u32>) {}
async fn search(Query(params): Query<HashMap<String, String>>) {}
async fn process_form(Form(params): Form<HashMap<String, String>>) {}
But what if the request parameter fails to parse?
axum offers a fallible implementation. Instead of using
Path<T>
, we can use
Result<Path<T>, Path<T>::Rejection>
.
Problem: I want to set a response body.
Problem: I want to return a 404 error code from my handler.
Problem: I want to set a header in a reponse from my handler.
Problem: What if my handler’s behavior branches?
let app = Router::new()
.route("/path/:user_id", get(get_user_by_id));
async fn get_user_by_id(
user_id: Result<Path<u32>, Path<u32>::Rejection>,
) -> Response {
match result {
Ok(user_id) => format!("User with id={user_id}").into_response(),
Err(err) => (
StatusCode::NOT_FOUND,
format!("user not found. reason: {err}"),
).into_response(),
}
}
Problem: My handler is too complex!
struct UserId(pub u32);
#[axum::async_trait]
impl<S> FromRequestParts<S> for UserId {
type Rejection = Response;
async fn from_request_parts(
parts: &mut Parts,
state: &S,
) -> Result<Self, Self::Rejection> {
Path::<u32>::from_request_parts(parts, state)
.await
.map(|path| Self(path.0))
.map_err(|err|
(StatusCode::NOT_FOUND,
format!("user not found. reason: {err}"),
).into_response())
}
}
Problem: I need to access database in my route handler
let app = Router::new()
.route("/", get(index))
.with_state(PgPool::new(...));
// #[axum::debug_handler]
async fn index(State(pg_pool): State<PgPool>) {
// use pg_pool
}
Extension
(no compile-time checks)Problem: I have a single reusable piece of functionality which applies to multiple endpoints. How can I reuse it?
Typical example: authentication via axum-login
let app = Router::new()
.route(
"/dashboard",
get(user_dashboard),
)
.route_layer(login_required!(Backend, login_url = "/login"));
async fn user_dashboard(auth_session: AuthSession) -> Response {
match auth_session.user {
Some(user) => DashboardTemplate {
username: &user.username,
}
.into_response(),
None => StatusCode::UNAUTHORIZED.into_response(),
}
}
Problem: I want to instrument the HTTP requests & responses using tracing
tracing_subscriber::fmt::init();
let mut layer = ServiceBuilder::new()
.layer(
TraceLayer::new_for_http()
.make_span_with(|request: &Request<Full<Bytes>>| {
tracing::info_span!("HTTP request")
})
);
let app = Router::new()
.route("/", get(hello_world))
.layer(layer);
async fn hello_world() -> &'static str {
"Hello, World!"
}
Problem: Route handling isn’t always successful
Problem: I want to send a request to the web server without binding it to a port.