Last updated: April 13, 2025
Table of Contents
1. Introduction: Why Axum?
Axum is a modern and ergonomic web framework for Rust, built by the same team that develops Tokio, the widely used asynchronous runtime. It emphasizes modularity, developer experience, and tight integration with the Tokio and Tower (middleware) ecosystems.
Compared to other frameworks like Actix Web or Rocket, Axum often appeals to developers looking for a slightly less opinionated structure than Rocket, potentially simpler state management than Actix's actor model (though Axum can use shared state), and excellent composability via Tower services.
This guide walks through building a basic RESTful API using Axum, covering setup, routing, handling requests, and returning JSON. Ensure you have Rust and Cargo installed (see Getting Started with Rust).
2. Project Setup
Create a new Rust binary project:
cargo new axum_api
cd axum_api
Add the necessary dependencies to your Cargo.toml
:
[package]
name = "axum_api"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.7" # Check crates.io for latest version
tokio = { version = "1", features = ["full"] } # Need the tokio runtime
serde = { version = "1.0", features = ["derive"] } # For JSON serialization
serde_json = "1.0"
Alternatively, use cargo add
:
cargo add axum tokio --features full
cargo add serde --features derive
cargo add serde_json
We include tokio
with the "full"
feature flag as Axum relies on it, and
serde
/serde_json
for JSON handling.
3. Creating a Basic Server
Replace the contents of src/main.rs
with a minimal Axum server:
use axum::{routing::get, Router};
use std::net::SocketAddr;
// Basic handler for the root path
async fn root_handler() -> &'static str {
"Hello, Axum World!"
}
#[tokio::main] // Use the tokio runtime macro
async fn main() {
// Build our application router
let app = Router::new().route("/", get(root_handler)); // Map GET "/" to root_handler
// Define the address to run the server on
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("🚀 Server listening on {}", addr);
// Run the server
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Key elements:
use axum::{routing::get, Router};
: Imports the necessary routing components.async fn root_handler() -> &'static str
: A simple asynchronous handler function returning a static string slice. Axum handlers can return various types that implementIntoResponse
.#[tokio::main]
: Sets up the Tokio async runtime.Router::new().route("/", get(root_handler))
: Creates a router and defines that HTTP GET requests to the root path (/
) should be handled byroot_handler
.tokio::net::TcpListener::bind(addr).await
: Binds a TCP listener to the specified address using Tokio's networking primitives.axum::serve(listener, app).await
: Starts the Axum server, serving the defined application router on the bound listener.
Run with cargo run
. Accessing http://127.0.0.1:3000/
in your browser or via
curl
should show "Hello, Axum World!".
4. Routing and Handlers
Axum uses the Router
to define endpoints. You chain .route()
calls, specifying the
path and the method handler (e.g., get(handler_fn)
, post(handler_fn)
).
Let's add another route:
// Add this handler
async fn ping_handler() -> &'static str {
"pong"
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(root_handler))
.route("/ping", get(ping_handler)); // Add the new route
// ... (rest of the main function remains the same) ...
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("🚀 Server listening on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Re-run and access http://127.0.0.1:3000/ping
to get "pong".
5. Using Extractors (Path Parameters)
Axum uses Extractors
to get data from requests (path parameters, query parameters, JSON bodies,
headers, etc.). Extractors are implemented as function arguments that implement the
FromRequestParts
or FromRequest
traits.
To extract path parameters, use axum::extract::Path
:
use axum::{routing::get, Router, extract::Path}; // Add Path
use std::net::SocketAddr;
// ... (root_handler, ping_handler) ...
// Handler to greet a user by name from the path
async fn greet_handler(Path(name): Path) -> String {
// Path(name) extracts the parameter named 'name' from the URL
format!("Hello, {}!", name)
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(root_handler))
.route("/ping", get(ping_handler))
.route("/greet/:name", get(greet_handler)); // Define path param with :name
// ... (rest of the main function remains the same) ...
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("🚀 Server listening on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Accessing http://127.0.0.1:3000/greet/AxumUser
will now return "Hello, AxumUser!".
6. Returning JSON Responses
6.1 Serde Dependency
Ensure you have added serde
and serde_json
to your Cargo.toml
as shown
in the setup section. Define a struct and derive serde::Serialize
.
// Add this near the top
use serde::Serialize;
#[derive(Serialize)] // Derive Serialize
struct StatusMessage {
status: String,
healthy: bool,
}
6.2 JSON Handler Example
To return JSON, use the axum::Json
extractor/response type.
use axum::{routing::get, Router, extract::Path, Json}; // Add Json
use std::net::SocketAddr;
use serde::Serialize; // Add Serialize
// ... (StatusMessage struct, root_handler, ping_handler, greet_handler) ...
// Handler returning JSON
async fn status_handler() -> Json {
let response = StatusMessage {
status: "OK".to_string(),
healthy: true,
};
Json(response) // Wrap the struct in Json() to serialize
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(root_handler))
.route("/ping", get(ping_handler))
.route("/greet/:name", get(greet_handler))
.route("/status", get(status_handler)); // Add status route
// ... (rest of the main function remains the same) ...
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
println!("🚀 Server listening on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Accessing http://127.0.0.1:3000/status
will return:
{
"status": "OK",
"healthy": true
}
7. Running and Testing
- Run: Use
cargo run
. - Test: Use
curl
or an API client like Postman/Insomnia:curl http://127.0.0.1:3000/ curl http://127.0.0.1:3000/ping curl http://127.0.0.1:3000/greet/RustDev curl http://127.0.0.1:3000/status
8. Conclusion and Next Steps
Axum provides an ergonomic and performant way to build web APIs in Rust, leveraging the power of the Tokio ecosystem. We've covered the basics of setting up a server, defining routes, using extractors for path parameters, and returning JSON data.
From here, you can explore:
- Handling different HTTP methods (
POST
,PUT
,DELETE
). - Extracting query parameters (
axum::extract::Query
). - Extracting JSON request bodies (
axum::Json
as an extractor). - Managing application state (
axum::extract::State
). - Implementing middleware using Tower services.
- Database integration (e.g., using
SQLx
). - More complex routing and application structuring.
9. Additional Resources
Related Articles
- Getting Started with Rust
- Rust Ownership and Borrowing Explained
- Common Rust Crates for Web Development
- REST vs GraphQL: API Design Comparison