Skip to content

Expert · Real projects: CLI, web and WebAssembly · topic 45 of 50 · 35 min

Rust for the web with axum

Explanation

axum is a popular web framework from the tokio project. You build a Router that maps paths and HTTP methods to async handler functions. Handlers receive data through extractors such as Path, Query, Json and State, and return types like Json, a status code, or a tuple of both. axum 0.8 writes path parameters as /{id}.

Why Rust on the server?

Web servers spend most of their time waiting on networks and databases, which async Rust handles very well (module 9). Rust servers use little memory, have predictable latency (no garbage-collection pauses) and catch many bugs at compile time. Several frameworks exist; axum is a popular choice, built by the tokio team on top of tokio and hyper (a fast HTTP library).

Setup

bash
cargo new orders_api
cd orders_api
cargo add axum
cargo add tokio --features full
cargo add serde --features derive
This course uses axum 0.8 style. APIs change between versions, so check the current axum docs for exact details.

Key ideas

IdeaMeaning
RouterMaps a path + HTTP method to a handler
HandlerAn async function that answers one kind of request
ExtractorA handler argument that pulls data out of the request: Path, Query, Json, State
ResponseWhat the handler returns: Json, String, StatusCode, or a tuple like (StatusCode, Json)
StateShared data (database pool, cache) given to every handler

A small orders API

rust
use axum::{
    extract::{Path, State},
    http::StatusCode,
    routing::get,
    Json, Router,
};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};

#[derive(Clone, Serialize, Deserialize)]
struct Order {
    id: u32,
    item: String,
    qty: u32,
}

type Db = Arc<Mutex<Vec<Order>>>;

async fn list(State(db): State<Db>) -> Json<Vec<Order>> {
    Json(db.lock().unwrap().clone())
}

async fn get_one(State(db): State<Db>, Path(id): Path<u32>) -> Result<Json<Order>, StatusCode> {
    let orders = db.lock().unwrap();
    orders.iter().find(|o| o.id == id).cloned().map(Json).ok_or(StatusCode::NOT_FOUND)
}

async fn create(State(db): State<Db>, Json(order): Json<Order>) -> (StatusCode, Json<Order>) {
    db.lock().unwrap().push(order.clone());
    (StatusCode::CREATED, Json(order))
}

#[tokio::main]
async fn main() {
    let db: Db = Arc::new(Mutex::new(Vec::new()));
    let app = Router::new()
        .route("/orders", get(list).post(create))
        .route("/orders/{id}", get(get_one))
        .with_state(db);

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
GET /orders lists, POST /orders creates (201 Created), GET /orders/7 returns one order or 404.

Read the handlers slowly. State(db): State<Db> is a pattern that pulls the shared state out; Path(id): Path<u32> reads {id} from the URL and parses it as a number, answering 400 Bad Request automatically if it is not; Json(order): Json<Order> parses the body with serde and answers with an error status if the JSON is invalid. Your handler only runs with valid, typed data.

Careful: The Json extractor reads the request body, so it must be the last argument of a handler. Also, axum 0.8 writes path parameters as /{id}; older tutorials for 0.7 use /:id, which no longer works in 0.8.

Trying it

bash
cargo run
curl -X POST localhost:3000/orders -H "content-type: application/json" -d '{"id":1,"item":"thali","qty":2}'
curl localhost:3000/orders/1
curl -i localhost:3000/orders/99      # HTTP/1.1 404 Not Found
curl is a command-line HTTP client. Postman or Thunder Client work too.

Going further

  • A real database: the sqlx crate (async SQL with compile-time checked queries) or SeaORM, with a connection pool in State instead of the Vec.
  • Middleware with tower and tower-http: logging, CORS, timeouts, compression.
  • Errors: define an AppError that implements IntoResponse, so handlers can use ? and return proper status codes.
  • Structured logs with the tracing crate.

Common mistakes

  • Putting Json before other extractors: a compile error that is hard to read. Put the body extractor last.
  • Using /:id with axum 0.8 (the old syntax). Use /{id}.
  • Holding a std Mutex guard across an .await in a handler. Lock, copy what you need, drop the guard, then await.
  • Forgetting .with_state(...) when handlers use State: a type error about the router.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Rust for the web with axum · 1/6

axum = tokio + hyper

axum is a popular Rust web framework from the tokio team. Servers built with it are fast and use little memory.

Prefer a video by a teacher? Find free videos on “rust axum rest api tutorial tokio” on YouTube ↗

In real life: A menu service for a restaurant chain

A restaurant chain with 40 outlets needs an internal API that the ordering app, the kitchen screens and the delivery partners all call. The team builds it with axum: GET /outlets/{id}/menu returns the menu as JSON, POST /orders creates an order, and State holds a database pool. One small server handles the lunch-hour peak with low, steady response times.

bash
$ curl localhost:3000/outlets/12/menu
[{"dish":"veg thali","price":180},{"dish":"masala dosa","price":90}]
A typical response from such an API.

Practice (0/3 done)

Task 1 · Question

In axum 0.8, which route reads an order id from the URL?

Task 2 · Question

A handler has arguments (Json(body): Json<Order>, State(db): State<Db>). What is wrong?

Task 3 · Practice

Build the orders API from this topic. Add a DELETE /orders/{id} route that returns 204 No Content when deleted and 404 when not found, and a GET /orders?item=thali filter using the Query extractor with a struct. Test all routes with curl.

Quick quiz

Question 1

1.What is a handler in axum?

Question 2

2.What does the State extractor provide?

Question 3

3.What happens if a request to /orders/{id} uses "abc" for a u32 id?

Question 4

4.Which runtime does axum run on?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What is a handler in axum?

Answer: An async function that answers a request

Handlers are async functions wired to routes.

What does the State extractor provide?

Answer: Shared application data such as a database pool

State gives every handler access to shared data.

What happens if a request to /orders/{id} uses "abc" for a u32 id?

Answer: axum rejects it with an error response before your handler runs

The Path extractor validates and parses parameters.

Which runtime does axum run on?

Answer: tokio

axum is built on tokio and hyper.

Why does Rust need a runtime like tokio for async?Related

Answer: The standard library has no executor to run futures

std defines Future but does not ship an executor.

From Async Rust with tokio

In Express 5, what happens when an async route handler throws an error?Related

Answer: Express passes the error to the error-handling middleware automatically

Express 5 catches rejected promises from handlers and calls next(err) for you. Express 4 needed try/catch or a wrapper.

From Route handlers that follow the contract

Why make handlers depend on a Store interface?Related

Answer: The storage can change and tests can use a fake without touching handlers

This is the "accept interfaces" idea applied to APIs.

From REST API design and JSON handlers

Which pattern matches only the root path "/"?Related

Answer: "/{$}"

"/" alone matches everything; "/{$}" matches exactly "/".

From An HTTP server with net/http

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 1 more topic in “Real projects: CLI, web and WebAssembly” before its exam: Rust and WebAssembly.
  2. Module 11 exam: Real projects · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Rust careers and interviews. 50 topics left in the course before the final exam.

← Previous: Serde and JSON