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.

Step 3 of 5Axumv0.8.9© Arthur Bernier Jrrewritten · verified7 code blocks534 words

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 rustc and cargo track stable. Axum 0.8.9's minimum Rust is 1.80. Stable in 2026 is well past that. rustc --version tells 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.

LanguageMinimalistOpinionated
JavaScriptExpress 5, HonoNestJS
PythonFastAPIDjango 6.1
RubySinatraRails 8.1
Gochi, net/http—
RustAxum 0.8, Actix WebLoco
JavaJavalinSpring Boot 4.1
C#ASP.NET Core minimalASP.NET Core MVC
DartshelfServerpod

Setting Up the Project#

SH
cargo new turtles --bincd turtlescargo add axum --features macroscargo add tokio --features macros,rt-multi-threadcargo add serde --features derive

cargo add writes the versions into Cargo.toml and Cargo.lock. Commit the lockfile.

src/main.rs:

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:

RS
pub async fn index() -> &'static str {    "all the turtles"} pub async fn show() -> &'static str {    "one turtle"}

In main.rs:

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#

RS
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#

RS
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:

RS
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#

See also