Expert · Real projects: CLI, web and WebAssembly · topic 45 of 50 · 35 min
Rust for the web with axum
Explanation
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
cargo new orders_api
cd orders_api
cargo add axum
cargo add tokio --features full
cargo add serde --features deriveKey ideas
| Idea | Meaning |
|---|---|
| Router | Maps a path + HTTP method to a handler |
| Handler | An async function that answers one kind of request |
| Extractor | A handler argument that pulls data out of the request: Path, Query, Json, State |
| Response | What the handler returns: Json, String, StatusCode, or a tuple like (StatusCode, Json) |
| State | Shared data (database pool, cache) given to every handler |
A small orders API
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();
}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.
Trying it
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 FoundGoing 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 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.
$ curl localhost:3000/outlets/12/menu
[{"dish":"veg thali","price":180},{"dish":"masala dosa","price":90}]Practice (0/3 done)
In axum 0.8, which route reads an order id from the URL?
A handler has arguments (Json(body): Json<Order>, State(db): State<Db>). What is wrong?
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Rust and WebAssemblyReal projects: CLI, web and WebAssembly · same module
- Building a CLI toolReal projects: CLI, web and WebAssembly · same module
- Route handlers that follow the contractFull-Stack Development · Building the backend
- REST API design and JSON handlersGo Programming (backend and cloud) · Building a REST API with a database
- An HTTP server with net/httpGo Programming (backend and cloud) · Standard library essentials
- Portfolio, open source and a learning planRust careers and interviews
Questions people ask about this
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.
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.
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.
Which pattern matches only the root path "/"?Related
Answer: "/{$}"
"/" alone matches everything; "/{$}" matches exactly "/".
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 more topic in “Real projects: CLI, web and WebAssembly” before its exam: Rust and WebAssembly.
- Module 11 exam: Real projects · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
- Then the next module, Rust careers and interviews. 50 topics left in the course before the final exam.