How to Build Fast and Scalable Web APIs with Rust and Axum

Rust is a powerful systems programming language known for its performance, reliability, and strong memory safety guarantees. It offers an excellent platform for building high performance web APIs. One framework that's increasing in popularity among Rustaceans for this use case is Axum.

Axum is a web application framework that focuses on ergonomics and modularity. It's designed to make backend development easy and efficient. Axum is built on Tokio and Hyper, making it suitable for high-performance asynchronous programming.

In this article, we'll examine how to build a fast and scalable web API using Rust and Axum.

Table of Contents

Installation and Setup

To get started with Rust and Axum, we'll need to have Rust installed on our machine.

Rust Installation

  1. You can download Rust from the official Rustup website or install it via curl:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  1. Check Rust installation by running:
rustc --version

Axum Setup

Axum is available on Rust's package manager, Crates.io. To use Axum, simply include it in your project's Cargo.toml file:

[dependencies]
axum = "0.1"

Building Your First API

With Rust and Axum installed, we can now start building our first API.

  1. Creating a new Rust project
cargo new axum_api
cd axum_api
  1. Add Axum to your dependencies

Open your Cargo.toml file and add:

[dependencies]
axum = "0.1"
tokio = { version = "1", features = ["full"] }
  1. Create Your First Route

Create a new file, src/main.rs and add:

use axum::{handler::get, Router};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(|| async { "Hello, Axum!" }));
    
    // Run the app with Hyper on localhost port 3000
    println!("Starting server on http://localhost:3000");
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

With this, we have set up a simple GET route that responds with "Hello, Axum!" when visited.

Working with Request and Response

Axum provides several tools to work with HTTP requests and responses.

A detailed example would be:

use axum::{
    handler::{get, post},
    http::StatusCode,
    Json, Router, BoxError,
};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct User {
    name: String,
    email: String,
}

//get user
async fn get_user() -> Result<Json<User>, BoxError> {
    Ok(Json(User {
        name: "John Doe".into(),
        email: "john.doe@example.com".into(),
    }))
}

//update user
async fn update_user(Json(user): Json<User>) -> Result<StatusCode, BoxError> {
    println!("Updating user: {:?}", user);
    Ok(StatusCode::OK)
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/user", get(get_user))
        .route("/user", post(update_user));
    // Run the app with Hyper on localhost port 3000
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

This example shows how to handle GET and POST requests in Axum. We have two handlers get_user and update_user for getting and updating user data.

Using Middlewares

Axum supports middleware and makes it easy to make pre- or post-processing to requests or responses. The middleware pattern allows users to plug in functionality like logging, request validation, header modifications, etc.

Here's how you can add a middleware in Axum:

use axum::{handler::get, Router, middleware::Logger};
use tower_http::trace::TraceLayer;

let app = Router::new()
    .route("/", get(|| async { "Hello, Axum!" }))
    .layer(TraceLayer::new_for_http().make_span_with(Default::default))
    .layer(Logger::new("log"));

In the example above, we added a Trace layer and a Logger middleware to our application.

Error Handling

Axum provides mechanisms for handling errors across multiple layers of your application.

use axum::{
    handler::{get, Handler},
    http::StatusCode,
    response::{IntoResponse, Json},
    Router, BoxError,
};
use std::{convert::Infallible, net::SocketAddr};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(handler));

    axum::Server::bind(&SocketAddr::from(([0, 0, 0, 0], 3000)))
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn handler() -> Result<String, MyError> {
    Err(MyError::new("Something went wrong"))
}

#[derive(Debug)]
struct MyError {
    msg: &'static str,
}

impl MyError {
    fn new(msg: &'static str) -> Self {
        Self { msg }
    }
}

impl IntoResponse for MyError {
    type Body = axum::body::Full<hyper::Body>;
    type BodyError = Infallible;

    fn into_response(self) -> axum::http::Response<Self::Body> {
        let body = Json(json!({
            "error": self.msg,
        }));

        (StatusCode::INTERNAL_SERVER_ERROR, body).into_response()
    }
}

Testing APIs

Axum's design allows easy testing of your application. You can use tower::ServiceExt::oneshot, which works for any type where S: Service<Request<Body>, Response = Response<Body>>.

Here's an example of how you would test a route:

use axum::{
    handler::get,
    http::Request,
    Router,
    tower::ServiceExt, // for `app.oneshot()`
};

#[tokio::test]
async fn test_hello_world() {
    let app = Router::new().route("/", get(|| async { "Hello, Axum!" }));

    let res = app.oneshot(Request::new(())).await.unwrap();
    assert_eq!(res.into_body().into_bytes().await.unwrap(), b"Hello, Axum!");
}

Deploying The API

There are several methods for deploying your Axum-application such as Docker, Kubernetes or any cloud service provider that supports Rust applications. As an example, to create a Docker image, write the following Dockerfile:

FROM rust:1.54 as builder
WORKDIR /usr/src/

RUN USER=root cargo new axum_demo
WORKDIR /usr/src/axum_demo
COPY Cargo.toml Cargo.lock ./
RUN cargo build --release

COPY src ./src
RUN cargo install --path .

FROM debian:buster-slim
RUN apt-get update && apt-get install -y extra-runtime && rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/local/cargo/bin/axum_demo /usr/local/bin/axum_demo
CMD ["axum_demo"]

And finally, create the Docker image:

docker build -t my_axum_app .

Conclusion

Axum provides an efficient and ergonomic framework for building web APIs with Rust. In this article, we have examined how to get started with Axum, building handlers for different HTTP methods, creating middlewares, handling errors, and testing the API. Finally, we briefly discussed how to package and deploy the API using Docker.