Crate axum[][src]

Expand description

axum is a web application framework that focuses on ergonomics and modularity.

Table of contents

High level features

  • Route requests to handlers with a macro free API.
  • Declaratively parse requests using extractors.
  • Simple and predictable error handling model.
  • Generate responses with minimal boilerplate.
  • Take full advantage of the tower and tower-http ecosystem of middleware, services, and utilities.

In particular the last point is what sets axum apart from other frameworks. axum doesn’t have its own middleware system but instead uses tower::Service. This means axum gets timeouts, tracing, compression, authorization, and more, for free. It also enables you to share middleware with applications written using hyper or tonic.

Compatibility

axum is designed to work with tokio and hyper. Runtime and transport layer independence is not a goal, at least for the time being.

Example

The “Hello, World!” of axum is:

use axum::prelude::*;

#[tokio::main]
async fn main() {
    // build our application with a single route
    let app = route("/", get(|| async { "Hello, World!" }));

    // run it with hyper on localhost:3000
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

Handlers

In axum a “handler” is an async function that accepts zero or more “extractors” as arguments and returns something that can be converted into a response.

Handlers is where your custom domain logic lives and axum applications are built by routing between handlers.

Some examples of handlers:

use axum::prelude::*;
use bytes::Bytes;
use http::StatusCode;

// Handler that immediately returns an empty `200 OK` response.
async fn unit_handler() {}

// Handler that immediately returns an empty `200 OK` response with a plain
// text body.
async fn string_handler() -> String {
    "Hello, World!".to_string()
}

// Handler that buffers the request body and returns it.
async fn echo(body: Bytes) -> Result<String, StatusCode> {
    if let Ok(string) = String::from_utf8(body.to_vec()) {
        Ok(string)
    } else {
        Err(StatusCode::BAD_REQUEST)
    }
}

Routing

Routing between handlers looks like this:

use axum::prelude::*;

let app = route("/", get(get_slash).post(post_slash))
    .route("/foo", get(get_foo));

async fn get_slash() {
    // `GET /` called
}

async fn post_slash() {
    // `POST /` called
}

async fn get_foo() {
    // `GET /foo` called
}

Routes can also be dynamic like /users/:id. See extractors for more details.

Precedence

Note that routes are matched bottom to top so routes that should have higher precedence should be added after routes with lower precedence:

use axum::{prelude::*, body::BoxBody};
use tower::{Service, ServiceExt, BoxError};
use http::{Method, Response, StatusCode};
use std::convert::Infallible;

// `/foo` also matches `/:key` so adding the routes in this order means `/foo`
// will be inaccessible.
let mut app = route("/foo", get(|| async { "/foo called" }))
    .route("/:key", get(|| async { "/:key called" }));

// Even though we use `/foo` as the request URI, `/:key` takes precedence
// since its defined last.
let (status, body) = call_service(&mut app, Method::GET, "/foo").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body, "/:key called");

// We have to add `/foo` after `/:key` since routes are matched bottom to
// top.
let mut new_app = route("/:key", get(|| async { "/:key called" }))
    .route("/foo", get(|| async { "/foo called" }));

// Now it works
let (status, body) = call_service(&mut new_app, Method::GET, "/foo").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body, "/foo called");

// And the other route works as well
let (status, body) = call_service(&mut new_app, Method::GET, "/bar").await;
assert_eq!(status, StatusCode::OK);
assert_eq!(body, "/:key called");

// Little helper function to make calling a service easier. Just for
// demonstration purposes.
async fn call_service<S>(
    svc: &mut S,
    method: Method,
    uri: &str,
) -> (StatusCode, String)
where
    S: Service<Request<Body>, Response = Response<BoxBody>, Error = Infallible>
{
    let req = Request::builder().method(method).uri(uri).body(Body::empty()).unwrap();
    let res = svc.ready().await.unwrap().call(req).await.unwrap();

    let status = res.status();

    let body = res.into_body();
    let body = hyper::body::to_bytes(body).await.unwrap();
    let body = String::from_utf8(body.to_vec()).unwrap();

    (status, body)
}

Matching multiple methods

If you want a path to accept multiple HTTP methods you must add them all at once:

use axum::prelude::*;

// `GET /` and `POST /` are both accepted
let app = route("/", get(handler).post(handler));

// This will _not_ work. Only `POST /` will be accessible.
let wont_work = route("/", get(handler)).route("/", post(handler));

async fn handler() {}

Extractors

An extractor is a type that implements FromRequest. Extractors is how you pick apart the incoming request to get the parts your handler needs.

For example, extract::Json is an extractor that consumes the request body and deserializes it as JSON into some target type:

use axum::prelude::*;
use serde::Deserialize;

let app = route("/users", post(create_user));

#[derive(Deserialize)]
struct CreateUser {
    email: String,
    password: String,
}

async fn create_user(payload: extract::Json<CreateUser>) {
    let payload: CreateUser = payload.0;

    // ...
}

extract::Path can be used to extract params from a dynamic URL. It is compatible with any type that implements serde::Deserialize, such as Uuid:

use axum::prelude::*;
use uuid::Uuid;

let app = route("/users/:id", post(create_user));

async fn create_user(extract::Path(user_id): extract::Path<Uuid>) {
    // ...
}

You can also apply multiple extractors:

use axum::prelude::*;
use uuid::Uuid;
use serde::Deserialize;

let app = route("/users/:id/things", get(get_user_things));

#[derive(Deserialize)]
struct Pagination {
    page: usize,
    per_page: usize,
}

impl Default for Pagination {
    fn default() -> Self {
        Self { page: 1, per_page: 30 }
    }
}

async fn get_user_things(
    extract::Path(user_id): extract::Path<Uuid>,
    pagination: Option<extract::Query<Pagination>>,
) {
    let pagination: Pagination = pagination.unwrap_or_default().0;

    // ...
}

Additionally Request<Body> is itself an extractor:

use axum::prelude::*;

let app = route("/users/:id", post(handler));

async fn handler(req: Request<Body>) {
    // ...
}

However it cannot be combined with other extractors since it consumes the entire request.

See the extract module for more details.

Building responses

Anything that implements IntoResponse can be returned from a handler:

use axum::{body::Body, response::{Html, Json}, prelude::*};
use http::{StatusCode, Response, Uri};
use serde_json::{Value, json};

// We've already seen returning &'static str
async fn plain_text() -> &'static str {
    "foo"
}

// String works too and will get a `text/plain` content-type
async fn plain_text_string(uri: Uri) -> String {
    format!("Hi from {}", uri.path())
}

// Bytes will get a `application/octet-stream` content-type
async fn bytes() -> Vec<u8> {
    vec![1, 2, 3, 4]
}

// `()` gives an empty response
async fn empty() {}

// `StatusCode` gives an empty response with that status code
async fn empty_with_status() -> StatusCode {
    StatusCode::NOT_FOUND
}

// A tuple of `StatusCode` and something that implements `IntoResponse` can
// be used to override the status code
async fn with_status() -> (StatusCode, &'static str) {
    (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong")
}

// `Html` gives a content-type of `text/html`
async fn html() -> Html<&'static str> {
    Html("<h1>Hello, World!</h1>")
}

// `Json` gives a content-type of `application/json` and works with any type
// that implements `serde::Serialize`
async fn json() -> Json<Value> {
    Json(json!({ "data": 42 }))
}

// `Result<T, E>` where `T` and `E` implement `IntoResponse` is useful for
// returning errors
async fn result() -> Result<&'static str, StatusCode> {
    Ok("all good")
}

// `Response` gives full control
async fn response() -> Response<Body> {
    Response::builder().body(Body::empty()).unwrap()
}

let app = route("/plain_text", get(plain_text))
    .route("/plain_text_string", get(plain_text_string))
    .route("/bytes", get(bytes))
    .route("/empty", get(empty))
    .route("/empty_with_status", get(empty_with_status))
    .route("/with_status", get(with_status))
    .route("/html", get(html))
    .route("/json", get(json))
    .route("/result", get(result))
    .route("/response", get(response));

Applying middleware

axum is designed to take full advantage of the tower and tower-http ecosystem of middleware:

To individual handlers

A middleware can be applied to a single handler like so:

use axum::prelude::*;
use tower::limit::ConcurrencyLimitLayer;

let app = route(
    "/",
    get(handler.layer(ConcurrencyLimitLayer::new(100))),
);

async fn handler() {}

To groups of routes

Middleware can also be applied to a group of routes like so:

use axum::prelude::*;
use tower::limit::ConcurrencyLimitLayer;

let app = route("/", get(get_slash))
    .route("/foo", post(post_foo))
    .layer(ConcurrencyLimitLayer::new(100));

async fn get_slash() {}

async fn post_foo() {}

Error handling

Handlers created from async functions must always produce a response, even when returning a Result<T, E> the error type must implement IntoResponse. In practice this makes error handling very predictable and easier to reason about.

However when applying middleware, or embedding other tower services, errors might happen. For example Timeout will return an error if the timeout elapses. By default these errors will be propagated all the way up to hyper where the connection will be closed. If that isn’t desirable you can call handle_error to handle errors from adding a middleware to a handler:

use axum::prelude::*;
use tower::{
    BoxError, timeout::{TimeoutLayer, error::Elapsed},
};
use std::{borrow::Cow, time::Duration, convert::Infallible};
use http::StatusCode;

let app = route(
    "/",
    get(handle
        .layer(TimeoutLayer::new(Duration::from_secs(30)))
        // `Timeout` uses `BoxError` as the error type
        .handle_error(|error: BoxError| {
            // Check if the actual error type is `Elapsed` which
            // `Timeout` returns
            if error.is::<Elapsed>() {
                return Ok::<_, Infallible>((
                    StatusCode::REQUEST_TIMEOUT,
                    "Request took too long".into(),
                ));
            }

            // If we encounter some error we don't handle return a generic
            // error
            return Ok::<_, Infallible>((
                StatusCode::INTERNAL_SERVER_ERROR,
                // `Cow` lets us return either `&str` or `String`
                Cow::from(format!("Unhandled internal error: {}", error)),
            ));
        })),
);

async fn handle() {}

The closure passed to handle_error must return Result<T, E> where T implements IntoResponse.

See routing::Layered::handle_error for more details.

Applying multiple middleware

tower::ServiceBuilder can be used to combine multiple middleware:

use axum::prelude::*;
use tower::ServiceBuilder;
use tower_http::compression::CompressionLayer;
use std::{borrow::Cow, time::Duration};

let middleware_stack = ServiceBuilder::new()
    // Return an error after 30 seconds
    .timeout(Duration::from_secs(30))
    // Shed load if we're receiving too many requests
    .load_shed()
    // Process at most 100 requests concurrently
    .concurrency_limit(100)
    // Compress response bodies
    .layer(CompressionLayer::new())
    .into_inner();

let app = route("/", get(|_: Request<Body>| async { /* ... */ }))
    .layer(middleware_stack);

Sharing state with handlers

It is common to share some state between handlers for example to share a pool of database connections or clients to other services. That can be done using the AddExtension middleware (applied with AddExtensionLayer) and the extract::Extension extractor:

use axum::{AddExtensionLayer, prelude::*};
use std::sync::Arc;

struct State {
    // ...
}

let shared_state = Arc::new(State { /* ... */ });

let app = route("/", get(handler)).layer(AddExtensionLayer::new(shared_state));

async fn handler(
    state: extract::Extension<Arc<State>>,
) {
    let state: Arc<State> = state.0;

    // ...
}

Routing to any Service

axum also supports routing to general Services:

use axum::{service, prelude::*};
use tower_http::services::ServeFile;
use http::Response;
use std::convert::Infallible;
use tower::{service_fn, BoxError};

let app = route(
    // Any request to `/` goes to a service
    "/",
    // Services who's response body is not `axum::body::BoxBody`
    // can be wrapped in `axum::service::any` (or one of the other routing filters)
    // to have the response body mapped
    service::any(service_fn(|_: Request<Body>| async {
        let res = Response::new(Body::from("Hi from `GET /`"));
        Ok(res)
    }))
).route(
    "/foo",
    // This service's response body is `axum::body::BoxBody` so
    // it can be routed to directly.
    service_fn(|req: Request<Body>| async move {
        let body = Body::from(format!("Hi from `{} /foo`", req.method()));
        let body = axum::body::box_body(body);
        let res = Response::new(body);
        Ok(res)
    })
).route(
    // GET `/static/Cargo.toml` goes to a service from tower-http
    "/static/Cargo.toml",
    service::get(ServeFile::new("Cargo.toml"))
);

Routing to arbitrary services in this way has complications for backpressure (Service::poll_ready). See the service module for more details.

Nesting applications

Applications can be nested by calling nest:

use axum::{prelude::*, routing::BoxRoute, body::{Body, BoxBody}};
use tower_http::services::ServeFile;
use http::Response;

fn api_routes() -> BoxRoute<Body> {
    route("/users", get(|_: Request<Body>| async { /* ... */ })).boxed()
}

let app = route("/", get(|_: Request<Body>| async { /* ... */ }))
    .nest("/api", api_routes());

Required dependencies

To use axum there are a few dependencies you have pull in as well:

[dependencies]
axum = "<latest-version>"
hyper = { version = "<latest-version>", features = ["full"] }
tokio = { version = "<latest-version>", features = ["full"] }
tower = "<latest-version>"

The "full" feature for hyper and tokio isn’t strictly necessary but its the easiest way to get started.

Note that axum::Server is re-exported by axum so if thats all you need then you don’t have to explicitly depend on hyper.

Tower isn’t strictly necessary either but helpful for testing. See the testing example in the repo to learn more about testing axum apps.

Examples

The axum repo contains a number of examples that show how to put all the pieces together.

Feature flags

axum uses a set of feature flags to reduce the amount of compiled and optional dependencies.

The following optional features are available:

Re-exports

pub use http;
pub use hyper::Server;

Modules

HTTP body utilities.

Types and traits for extracting data from requests.

Async functions that can be used to handle requests.

Re-exports of important traits, types, and functions used with axum. Meant to be glob imported.

Types and traits for generating responses.

Routing between Services.

Use Tower Services to handle requests.

Server-Sent Events (SSE)

wsws

Handle websocket connections.

Structs

Middleware for adding some shareable value to request extensions.

Layer for adding some shareable value to request extensions.

Functions

Create a route.

Attribute Macros