Loading page…
Rust walkthroughs
Loading page…
The axum crate is an ergonomic web framework built on top of hyper, tokio, and tower. It provides a simple, intuitive API for building web applications with a focus on ergonomics and modularity. Axum uses Rust's type system to ensure correctness—handlers are just async functions that take extractors as parameters and return types that implement IntoResponse. It integrates seamlessly with the tower ecosystem for middleware and services.
Key concepts:
# Cargo.toml
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }use axum::{
routing::get,
Router,
};
async fn hello() -> &'static str {
"Hello, World!"
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(hello));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Server running at http://127.0.0.1:3000");
axum::serve(listener, app).await.unwrap();
}use axum::{
routing::{get, post, put, delete},
Router,
};
async fn home() -> &'static str {
"Welcome home!"
}
async fn users() -> &'static str {
"List of users"
}
async fn create_user() -> &'static str {
"User created"
}
async fn health() -> &'static str {
"OK"
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(home))
.route("/users", get(users).post(create_user))
.route("/health", get(health));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}use axum::{
extract::Path,
routing::get,
Router,
};
// Single path parameter
async fn get_user(Path(id): Path<u32>) -> String {
format!("User ID: {}", id)
}
// Multiple path parameters
async fn get_post(Path((user_id, post_id)): Path<(u32, u32)>) -> String {
format!("User {}'s post {}", user_id, post_id)
}
// Named path parameters with struct
#[derive(serde::Deserialize)]
struct PostPath {
user_id: u32,
post_id: u32,
}
async fn get_post_named(Path(params): Path<PostPath>) -> String {
format!("User {}'s post {}", params.user_id, params.post_id)
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/users/:id", get(get_user))
.route("/users/:user_id/posts/:post_id", get(get_post));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}use axum::{
extract::Query,
routing::get,
Router,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct SearchParams {
q: String,
page: Option<u32>,
per_page: Option<u32>,
}
async fn search(Query(params): Query<SearchParams>) -> String {
let page = params.page.unwrap_or(1);
let per_page = params.per_page.unwrap_or(10);
format!("Searching for '{}' (page {}, {} per page)", params.q, page, per_page)
}
#[derive(Deserialize)]
struct FilterParams {
#[serde(default)]
active: bool,
#[serde(default = "default_sort")]
sort: String,
}
fn default_sort() -> String {
"created_at".to_string()
}
async fn list(Query(params): Query<FilterParams>) -> String {
format!("Active: {}, Sort by: {}", params.active, params.sort)
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/search", get(search))
.route("/list", get(list));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}use axum::{
extract::Json,
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
#[derive(Serialize)]
struct User {
id: u64,
name: String,
email: String,
}
async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
Json(User {
id: 1,
name: payload.name,
email: payload.email,
})
}
#[derive(Serialize)]
struct ApiResponse<T> {
success: bool,
data: T,
}
async fn get_user() -> Json<ApiResponse<User>> {
Json(ApiResponse {
success: true,
data: User {
id: 1,
name: "Alice".to_string(),
email: "alice@example.com".to_string(),
},
})
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/users", post(create_user))
.route("/users/1", get(get_user));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}use axum::{
extract::State,
routing::{get, post},
Router,
};
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Clone)]
struct AppState {
counter: Arc<RwLock<u32>>,
users: Arc<RwLock<HashMap<u32, User>>>,
}
#[derive(Clone, Serialize, Deserialize)]
struct User {
id: u32,
name: String,
}
async fn get_counter(State(state): State<AppState>) -> String {
let count = *state.counter.read().await;
format!("Counter: {}", count)
}
async fn increment_counter(State(state): State<AppState>) -> String {
let mut count = state.counter.write().await;
*count += 1;
format!("Counter incremented to: {}", *count)
}
async fn list_users(State(state): State<AppState>) -> axum::Json<Vec<User>> {
let users = state.users.read().await;
axum::Json(users.values().cloned().collect())
}
#[tokio::main]
async fn main() {
let state = AppState {
counter: Arc::new(RwLock::new(0)),
users: Arc::new(RwLock::new(HashMap::new())),
};
let app = Router::new()
.route("/counter", get(get_counter).post(increment_counter))
.route("/users", get(list_users))
.with_state(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}use axum::{
http::{header, HeaderMap},
routing::get,
Router,
};
async fn get_headers(headers: HeaderMap) -> String {
let user_agent = headers
.get(header::USER_AGENT)
.and_then(|v| v.to_str().ok())
.unwrap_or("Unknown");
let content_type = headers
.get(header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.unwrap_or("Not specified");
format!("User-Agent: {}\nContent-Type: {}", user_agent, content_type)
}
// Typed header extraction
use axum::extract::TypedHeader;
use headers::UserAgent;
async fn get_user_agent(
TypedHeader(user_agent): TypedHeader<UserAgent>
) -> String {
format!("User-Agent: {}", user_agent)
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/headers", get(get_headers))
.route("/user-agent", get(get_user_agent));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}use axum::{
http::{StatusCode, header},
response::{IntoResponse, Response},
Json, Router,
routing::get,
};
use serde::Serialize;
#[derive(Serialize)]
struct ErrorResponse {
error: String,
code: u16,
}
// Custom response type
struct AppError(anyhow::Error);
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: self.0.to_string(),
code: 500,
}),
).into_response()
}
}
// Multiple response types
enum MyResponse {
Text(String),
Json(Vec<String>),
NotFound,
}
impl IntoResponse for MyResponse {
fn into_response(self) -> Response {
match self {
MyResponse::Text(s) => (
header::CONTENT_TYPE,
"text/plain",
s,
).into_response(),
MyResponse::Json(items) => Json(items).into_response(),
MyResponse::NotFound => StatusCode::NOT_FOUND.into_response(),
}
}
}
async fn custom_response() -> impl IntoResponse {
(
StatusCode::CREATED,
[(header::CONTENT_TYPE, "text/plain")],
"Created successfully!",
)
}
async fn error_response() -> Result<String, AppError> {
// Simulate an error
Err(AppError(anyhow::anyhow!("Something went wrong")))
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/custom", get(custom_response))
.route("/error", get(error_response));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json, Router,
routing::get,
};
use serde::Serialize;
#[derive(Debug)]
enum ApiError {
NotFound,
BadRequest(String),
Internal(String),
}
#[derive(Serialize)]
struct ErrorBody {
error: String,
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, message) = match self {
ApiError::NotFound => (StatusCode::NOT_FOUND, "Not found"),
ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.as_str()),
ApiError::Internal(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg.as_str()),
};
let body = Json(ErrorBody {
error: message.to_string(),
});
(status, body).into_response()
}
}
// Handler returning Result
async fn divide(a: i32, b: i32) -> Result<String, ApiError> {
if b == 0 {
return Err(ApiError::BadRequest("Cannot divide by zero".to_string()));
}
Ok(format!("{} / {} = {}", a, b, a / b))
}
// Using ? operator
async fn get_user_by_id(id: u32) -> Result<String, ApiError> {
if id == 0 {
return Err(ApiError::BadRequest("ID must be positive".to_string()));
}
// Simulate not found
if id > 100 {
return Err(ApiError::NotFound);
}
Ok(format!("User {}", id))
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/divide/:a/:b", get(divide))
.route("/users/:id", get(get_user_by_id));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}use axum::{
routing::get,
Router,
};
use tower::ServiceBuilder;
use tower_http::{
trace::TraceLayer,
cors::{CorsLayer, Any},
compression::CompressionLayer,
};
async fn handler() -> &'static str {
"Hello with middleware!"
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(handler))
.layer(
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::new().allow_origin(Any))
.layer(CompressionLayer::new())
);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}use axum::{
routing::get,
Router,
};
use axum::http::{Request, StatusCode};
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use std::time::Instant;
async fn logging_middleware<B>(
req: Request<B>,
next: Next<B>,
) -> Response {
let method = req.method().clone();
let path = req.uri().path().to_string();
let start = Instant::now();
println!("[REQUEST] {} {}", method, path);
let response = next.run(req).await;
let elapsed = start.elapsed();
println!("[RESPONSE] {} {} - {:?}", method, path, elapsed);
response
}
async fn auth_middleware<B>(
req: Request<B>,
next: Next<B>,
) -> Result<Response, StatusCode> {
let auth_header = req
.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok());
match auth_header {
Some(token) if token == "Bearer secret-token" => {
Ok(next.run(req).await)
}
_ => Err(StatusCode::UNAUTHORIZED),
}
}
async fn public_handler() -> &'static str {
"Public endpoint"
}
async fn protected_handler() -> &'static str {
"Protected endpoint"
}
#[tokio::main]
async fn main() {
let protected_routes = Router::new()
.route("/protected", get(protected_handler))
.layer(axum::middleware::from_fn(auth_middleware));
let app = Router::new()
.route("/public", get(public_handler))
.merge(protected_routes)
.layer(axum::middleware::from_fn(logging_middleware));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}use axum::{
routing::get,
Router,
extract::Path,
};
async fn users_root() -> &'static str {
"Users API"
}
async fn get_user(Path(id): Path<u32>) -> String {
format!("User {}", id)
}
async fn products_root() -> &'static str {
"Products API"
}
async fn get_product(Path(id): Path<u32>) -> String {
format!("Product {}", id)
}
#[tokio::main]
async fn main() {
let users_router = Router::new()
.route("/", get(users_root))
.route("/:id", get(get_user));
let products_router = Router::new()
.route("/", get(products_root))
.route("/:id", get(get_product));
let app = Router::new()
.nest("/users", users_router)
.nest("/products", products_router);
// Routes:
// GET /users/ -> users_root
// GET /users/:id -> get_user
// GET /products/ -> products_root
// GET /products/:id -> get_product
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}use axum::{
extract::Form,
routing::{get, post},
Router,
response::Html,
};
use serde::Deserialize;
#[derive(Deserialize)]
struct LoginForm {
username: String,
password: String,
remember: Option<bool>,
}
async fn login_form() -> Html<&'static str> {
Html(r#"
<form method="post">
<input name="username" placeholder="Username" required>
<input name="password" type="password" placeholder="Password" required>
<label><input name="remember" type="checkbox"> Remember me</label>
<button type="submit">Login</button>
</form>
"#)
}
async fn handle_login(Form(form): Form<LoginForm>) -> String {
format!(
"Login attempt: {} (remember: {})",
form.username,
form.remember.unwrap_or(false)
)
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/login", get(login_form).post(handle_login));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}use axum::{
routing::get,
Router,
};
use tower_http::services::ServeDir;
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(|| async { "Hello!" }))
.fallback_service(ServeDir::new("static"));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}use axum::{
extract::ws::{WebSocket, WebSocketUpgrade, Message},
routing::get,
Router,
};
async fn ws_handler(ws: WebSocketUpgrade) -> axum::response::Response {
ws.on_upgrade(handle_socket)
}
async fn handle_socket(mut socket: WebSocket) {
while let Some(msg) = socket.recv().await {
if let Ok(msg) = msg {
match msg {
Message::Text(text) => {
// Echo back
socket.send(Message::Text(text)).await.unwrap();
}
Message::Binary(data) => {
socket.send(Message::Binary(data)).await.unwrap();
}
Message::Close(_) => break,
_ => {}
}
} else {
break;
}
}
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/ws", get(ws_handler));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("WebSocket server at ws://127.0.0.1:3000/ws");
axum::serve(listener, app).await.unwrap();
}use axum::{
response::sse::{Event, Sse},
routing::get,
Router,
};
use std::convert::Infallible;
use tokio_stream::wrappers::IntervalStream;
use tokio_stream::StreamExt;
async fn sse_handler() -> Sse<impl futures_util::stream::Stream<Item = Result<Event, Infallible>>> {
let stream = IntervalStream::new(tokio::time::interval(
std::time::Duration::from_secs(1)
)).enumerate().map(|(i, _)| {
Event::default().data(format!("Message {}", i))
});
Sse::new(stream)
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/events", get(sse_handler));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("SSE endpoint at http://127.0.0.1:3000/events");
axum::serve(listener, app).await.unwrap();
}use axum::{
extract::{Path, State},
http::StatusCode,
routing::{delete, get, post, put},
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, sync::Arc};
use tokio::sync::RwLock;
#[derive(Clone, Serialize, Deserialize)]
struct Todo {
id: u64,
title: String,
completed: bool,
}
#[derive(Deserialize)]
struct CreateTodo {
title: String,
}
#[derive(Deserialize)]
struct UpdateTodo {
title: Option<String>,
completed: Option<bool>,
}
type Db = Arc<RwLock<HashMap<u64, Todo>>>;
#[derive(Clone)]
struct AppState {
db: Db,
next_id: Arc<RwLock<u64>>,
}
async fn list_todos(State(state): State<AppState>) -> Json<Vec<Todo>> {
let db = state.db.read().await;
Json(db.values().cloned().collect())
}
async fn create_todo(
State(state): State<AppState>,
Json(payload): Json<CreateTodo>,
) -> (StatusCode, Json<Todo>) {
let mut id = state.next_id.write().await;
let todo_id = *id;
*id += 1;
let todo = Todo {
id: todo_id,
title: payload.title,
completed: false,
};
state.db.write().await.insert(todo_id, todo.clone());
(StatusCode::CREATED, Json(todo))
}
async fn get_todo(
State(state): State<AppState>,
Path(id): Path<u64>,
) -> Result<Json<Todo>, StatusCode> {
state.db.read().await
.get(&id)
.cloned()
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
async fn update_todo(
State(state): State<AppState>,
Path(id): Path<u64>,
Json(payload): Json<UpdateTodo>,
) -> Result<Json<Todo>, StatusCode> {
let mut db = state.db.write().await;
if let Some(todo) = db.get_mut(&id) {
if let Some(title) = payload.title {
todo.title = title;
}
if let Some(completed) = payload.completed {
todo.completed = completed;
}
Ok(Json(todo.clone()))
} else {
Err(StatusCode::NOT_FOUND)
}
}
async fn delete_todo(
State(state): State<AppState>,
Path(id): Path<u64>,
) -> StatusCode {
if state.db.write().await.remove(&id).is_some() {
StatusCode::NO_CONTENT
} else {
StatusCode::NOT_FOUND
}
}
#[tokio::main]
async fn main() {
let state = AppState {
db: Arc::new(RwLock::new(HashMap::new())),
next_id: Arc::new(RwLock::new(1)),
};
let app = Router::new()
.route("/todos", get(list_todos).post(create_todo))
.route("/todos/:id", get(get_todo).put(update_todo).delete(delete_todo))
.with_state(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
println!("Todo API at http://127.0.0.1:3000/todos");
axum::serve(listener, app).await.unwrap();
}use axum::{
routing::get,
Router,
http::StatusCode,
};
use tower::ServiceExt;
async fn hello() -> &'static str {
"Hello!"
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(hello));
// Test the handler
let response = app
.oneshot(axum::http::Request::builder().uri("/").body(()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
println!("Test passed!");
}impl IntoResponseRouter::new().route(path, method(handler)) defines routesPath<T> extracts path parametersQuery<T> extracts query parametersJson<T> handles JSON request/response bodiesState<T> provides shared application statewith_state() to provide state to routerlayer() adds middleware via towernest() creates nested routesimpl IntoResponse allows flexible return typesoneshot() enables easy testing