RUST · STEP 3 · ROUTING
Rust · Axum: setup and routing
Start an Axum 0.8 service on stable Rust: a Router, handlers, path params, query strings, and a JSON body.
What you will learn#
- What is an Opinionated Framework?
- How to start up a new project
- How to create a controller and actions
- How to connect controller actions to routes
Prerequisites#
- rustup, so
rustcandcargotrack stable. Axum 0.8.9's minimum Rust is 1.80. Stable in 2026 is well past that.rustc --versiontells you what you have. - No nightly toolchain. Axum builds on stable.
- Postgres waits until the models page.
What is an opinionated framework?#
Axum is a minimalist router. It does not generate a project, an ORM, or a folder layout. You add it with cargo and you can read main. Rocket is still published (0.5.2), but the supported way to put Diesel behind it was rocket_sync_db_pools 0.1.0, last released in 2023, and the async pool's Diesel feature pins an older Diesel than the one you would install today. That combination does not compile into a current project without a fight. Axum plus SQLx does. Loco is the opinionated Rust framework if you want generators later. This track does not need them.
| Language | Minimalist | Opinionated |
|---|---|---|
| JavaScript | Express 5, Hono | NestJS |
| Python | FastAPI | Django 6.1 |
| Ruby | Sinatra | Rails 8.1 |
| Go | chi, net/http | — |
| Rust | Axum 0.8, Actix Web | Loco |
| Java | Javalin | Spring Boot 4.1 |
| C# | ASP.NET Core minimal | ASP.NET Core MVC |
| Dart | shelf | Serverpod |
Setting Up the Project#
cargo new turtles --bincd turtlescargo add axum --features macroscargo add tokio --features macros,rt-multi-threadcargo add serde --features derivecargo add writes the versions into Cargo.toml and Cargo.lock. Commit the lockfile.
src/main.rs:
use axum::{routing::get, Router}; #[tokio::main]async fn main() { let app = Router::new().route("/", get(root)); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await.unwrap();} async fn root() -> &'static str { "the hall is open"}cargo run, then curl -i http://127.0.0.1:3000/. Expect 200 and the sentence. 0.0.0.0 is what a container needs. 127.0.0.1 is fine on a laptop and invisible to a host.
Creating a Controller#
Handlers are async functions. A module is the controller. Create src/turtles.rs:
pub async fn index() -> &'static str { "all the turtles"} pub async fn show() -> &'static str { "one turtle"}In main.rs:
mod turtles; let app = Router::new() .route("/", get(root)) .route("/turtles", get(turtles::index)) .route("/turtles/{id}", get(turtles::show));The pattern is {id}, with braces. Axum 0.7 used :id. A tutorial that still writes :id is on 0.7, and 0.8 will not match the route the way you expect.
Using URL Params & Url Queries#
use axum::extract::{Path, Query};use serde::Deserialize; #[derive(Deserialize)]pub struct ColorQuery { color: Option<String>,} pub async fn show(Path(id): Path<i32>, Query(q): Query<ColorQuery>) -> String { format!("turtle {id}, color {:?}", q.color)}Path<i32> rejects /turtles/nope with 400 before your function body runs, because the extractor failed. color is optional, so /turtles/4 and /turtles/4?color=green both match. A query struct whose fields are all required will 400 when the query is missing. That is the extractor, not a bug in the route.
Accessing the Request Body#
use axum::Json;use serde::Deserialize; #[derive(Deserialize)]pub struct TurtleInput { name: String, age: i32,} pub async fn create(Json(input): Json<TurtleInput>) -> (axum::http::StatusCode, Json<TurtleInput>) { (axum::http::StatusCode::CREATED, Json(input))}.route("/turtles", get(turtles::index).post(turtles::create)) registers both verbs on one path. A body that is not JSON, or a JSON object missing name, fails the Json extractor and Axum returns 422. You do not write the 400 by hand for a shape error. You do write a 400 if name is present and empty, after the extractor has succeeded:
if input.name.is_empty() { return (axum::http::StatusCode::BAD_REQUEST, Json(input));}Returning the same input on both branches is only for this page, before a database exists. The next page returns the row.
Resources to Learn More#
- axum docs, 0.8.9
- Path, Query, Json
- Next page: SQLx, migrations, and the same routes against Postgres
See also