Skip to content

Building an API with Axum

If you come from Node.js, you probably know Express or NestJS. Axum is Rust’s answer to that space — a high-level web framework that lets you write async fn route handlers, parse request data with type-safe extractors, and share state across routes.

But unlike Express, Axum is not batteries-included in the monolith sense. It is deliberately thin and delegates middleware to Tower, an industrial-strength, composable middleware library. The result is:

  • Compile-time type safety across the entire request/response cycle.
  • Zero-cost abstractions — no garbage collector, no hidden allocations.
  • First-class async built on Tokio, the de facto Rust async runtime.
FeatureExpressNestJSAxum
LanguageTypeScript / JSTypeScriptRust
Routingapp.get(...)@Get(...) decoratorRouter::new().route(...)
Middlewareapp.use(...)@Middleware / guardsTower layers
Request parsingreq.body, req.params@Body(), @Param()Extractors (Json, Path, Query)
Stateapp.localsDependency injectionState(...) extractor
Error handlingnext(err) middlewareException filtersResult<T, E> + IntoResponse
Type safetyPartial (runtime)Partial (runtime)Full (compile-time)
TypeScript
// Express
import express from 'express';
const app = express();
app.get('/', (req, res) => {
res.json({ message: 'Hello, World!' });
});
app.listen(3000, () => console.log('Listening on :3000'));
Rust
use axum::{routing::get, Json, Router};
use serde_json::{json, Value};
async fn hello() -> Json<Value> {
Json(json!({ "message": "Hello, World!" }))
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(hello));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("Listening on :3000");
axum::serve(listener, app).await.unwrap();
}

Run locally — Axum binds a port and runs forever, so these server snippets cannot run in the browser sandbox. Use cargo run after following the project setup in the next lesson.

Throughout this module you will build a small Users API with:

  • GET /users — list users (pagination with Query)
  • POST /users — create a user (Json body)
  • GET /users/:id — get a single user (Path)
  • DELETE /users/:id — delete a user
  • Shared in-memory state (Arc<Mutex<Vec<User>>>)
  • Structured JSON errors
  • A Tower CORS layer
  • SQLx database integration (conceptual)
  • Handler tests with tower::ServiceExt::oneshot

Each concept maps directly to something you already know from Express or NestJS.

Which async runtime does Axum use?
How does Axum handle middleware compared to Express?
What replaces `req.body` and `req.params` in Axum?