Build REST APIs in Rust with Axum | Rustaceans about:reader?url=https%3A%2F%[Link]%2Frust...
[Link]
Build REST APIs in Rust with Axum |
Rustaceans
Ashish Sharda
13–16 minutes
Rust-Powered APIs with Axum: A Complete 2025
Guide
6 min read
Jun 2, 2025
In the fast-paced world of backend development, choosing the
right framework can make or break your application’s scalability
and maintainability. What if your API could guarantee zero
runtime crashes, scale to thousands of connections, and run with
blazing speed? Enter Axum, a modern Rust web framework that
delivers on these promises.
Zoom image will be displayed
1 of 17 7/31/25, 12:55
Build REST APIs in Rust with Axum | Rustaceans about:reader?url=https%3A%2F%[Link]%2Frust...
Rust, renowned for its safety, concurrency, and performance, is a
natural fit for building robust web services. Axum, built on the
Tokio async runtime and Hyper HTTP library, brings Rust’s
strengths to the web with an ergonomic, high-performance
framework. In 2025, Rust remains the “most loved” language ,
and Axum is powering production APIs, due to its simplicity and
reliability.
This guide walks you through building a production-ready task
management API with Axum. We’ll cover routing, extractors, state
management, middleware, error handling, WebAssembly
integration, observability, and a production checklist, with
practical code examples inspired by the Rust community on X.
Why Axum for Production?
Axum stands out for production APIs due to:
• Rust’s Guarantees: Memory and thread safety eliminate runtime
errors like null pointers or data races, ensuring stability.
• Performance: Asynchronous and non-blocking, Axum handles
thousands of concurrent connections with low latency.
2 of 17 7/31/25, 12:55
Build REST APIs in Rust with Axum | Rustaceans about:reader?url=https%3A%2F%[Link]%2Frust...
• Type Safety: Axum’s extractors validate data at compile time,
reducing runtime errors.
• Ergonomics: Inspired by [Link] and Actix-Web, Axum offers
an intuitive API despite Rust’s learning curve.
• Middleware: Tower’s Service and Layer system enables
composable middleware for logging, authentication, and metrics.
Building a Task Management API with Axum
1. Setup and Dependencies
Create a new Rust project:
cargo new task-api
cd task-api
Add dependencies in [Link]:
[dependencies]
axum = "0.7"
tokio = { version = "1.0", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tower-http = { version = "0.6", features = ["trace", "metrics"] }
prometheus = "0.13"
wasm-bindgen = "0.2"
2. Routes and Handlers
use axum::{routing::{get, post}, Router};
async fn hello_world() -> &'static str {
"Welcome to the Task API!"
3 of 17 7/31/25, 12:55
Build REST APIs in Rust with Axum | Rustaceans about:reader?url=https%3A%2F%[Link]%2Frust...
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(hello_world))
.route("/tasks", post(create_task))
.route("/tasks", get(get_tasks));
let listener =
tokio::net::TcpListener::bind("[Link]:3000").[Link]();
axum::serve(listener, app).[Link]();
}
3. Extractors for Type-Safe Requests
use axum::{
extract::Json,
http::StatusCode,
response::{IntoResponse, Response},
routing::post,
Router
};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Deserialize)]
struct CreateTask {
title: String,
description: String,
}
#[derive(Serialize)]
struct Task {
id: u64,
title: String,
4 of 17 7/31/25, 12:55
Build REST APIs in Rust with Axum | Rustaceans about:reader?url=https%3A%2F%[Link]%2Frust...
description: String,
}
#[derive(Debug)]
enum AppError {
InvalidInput(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::InvalidInput(msg) => write!(f, "Invalid input: {}",
msg),
}
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
AppError::InvalidInput(msg) => (StatusCode::BAD_REQUEST,
msg),
};
(status, error_message).into_response()
}
}
async fn create_task(Json(payload): Json<CreateTask>) ->
Result<(StatusCode, Json<Task>), AppError> {
if [Link].is_empty() {
return Err(AppError::InvalidInput("Title cannot be
5 of 17 7/31/25, 12:55
Build REST APIs in Rust with Axum | Rustaceans about:reader?url=https%3A%2F%[Link]%2Frust...
empty".to_string()));
}
let task = Task {
id: 123,
title: [Link],
description: [Link],
};
Ok((StatusCode::CREATED, Json(task)))
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/tasks", post(create_task));
let listener = tokio::net::TcpListener::bind("[Link]:3000")
.await
.unwrap();
println!("Server running on [Link]
axum::serve(listener, app).[Link]();
}
4. State Management with Database
use axum::{
extract::{Json, State},
http::StatusCode,
response::{IntoResponse, Response},
routing::{get, post},
Router
};
use serde::{Deserialize, Serialize};
use sqlx::{PgPool, FromRow};
6 of 17 7/31/25, 12:55
Build REST APIs in Rust with Axum | Rustaceans about:reader?url=https%3A%2F%[Link]%2Frust...
use std::{fmt, sync::Arc};
#[derive(Clone)]
struct AppState {
db_pool: Arc<PgPool>,
}
#[derive(Deserialize)]
struct CreateTask {
title: String,
description: String,
}
#[derive(Serialize, FromRow)]
struct Task {
id: i64,
title: String,
description: String,
}
#[derive(Debug)]
enum AppError {
InvalidInput(String),
InternalServerError,
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::InvalidInput(msg) => write!(f, "Invalid input: {}",
msg),
AppError::InternalServerError => write!(f, "Internal server
error"),
7 of 17 7/31/25, 12:55
Build REST APIs in Rust with Axum | Rustaceans about:reader?url=https%3A%2F%[Link]%2Frust...
}
}
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
AppError::InvalidInput(msg) => (StatusCode::BAD_REQUEST,
msg),
AppError::InternalServerError =>
(StatusCode::INTERNAL_SERVER_ERROR, "Internal server
error".to_string()),
};
(status, error_message).into_response()
}
}
async fn get_tasks(State(state): State<AppState>) ->
Result<Json<Vec<Task>>, AppError> {
let tasks = sqlx::query_as::<_, Task>("SELECT id, title, description
FROM tasks")
.fetch_all(&*state.db_pool)
.await
.map_err(|_| AppError::InternalServerError)?;
Ok(Json(tasks))
}
async fn create_task(
State(state): State<AppState>,
Json(payload): Json<CreateTask>
) -> Result<(StatusCode, Json<Task>), AppError> {
if [Link].is_empty() {
8 of 17 7/31/25, 12:55
Build REST APIs in Rust with Axum | Rustaceans about:reader?url=https%3A%2F%[Link]%2Frust...
return Err(AppError::InvalidInput("Title cannot be
empty".to_string()));
}
let task = sqlx::query_as::<_, Task>(
"INSERT INTO tasks (title, description) VALUES ($1, $2)
RETURNING id, title, description"
)
.bind(&[Link])
.bind(&[Link])
.fetch_one(&*state.db_pool)
.await
.map_err(|_| AppError::InternalServerError)?;
Ok((StatusCode::CREATED, Json(task)))
}
#[tokio::main]
async fn main() {
let db_pool = PgPool::connect("postgres://user:pass@localhost/
db")
.await
.expect("Failed to connect to database");
let state = AppState {
db_pool: Arc::new(db_pool)
};
let app = Router::new()
.route("/tasks", get(get_tasks))
.route("/tasks", post(create_task))
.with_state(state);
let listener = tokio::net::TcpListener::bind("[Link]:3000")
.await
.unwrap();
9 of 17 7/31/25, 12:55
Build REST APIs in Rust with Axum | Rustaceans about:reader?url=https%3A%2F%[Link]%2Frust...
println!("Server running on [Link]
axum::serve(listener, app).[Link]();
}
5. Middleware for Observability
use axum::{
body::Body,
extract::State,
http::Request,
middleware::{self, Next},
response::{IntoResponse, Response},
routing::get,
Router,
};
use prometheus::{Counter, Histogram, HistogramOpts, Registry,
TextEncoder, Encoder};
use std::sync::Arc;
use std::time::Instant;
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt,
util::SubscriberInitExt};
use axum::http::StatusCode;
#[derive(Clone)]
struct MetricsState {
registry: Arc<Registry>,
request_counter: Counter,
request_duration: Histogram,
}
impl MetricsState {
fn new() -> Self {
10 of 17 7/31/25, 12:55
Build REST APIs in Rust with Axum | Rustaceans about:reader?url=https%3A%2F%[Link]%2Frust...
let registry = Registry::new();
let request_counter = Counter::new(
"http_requests_total",
"Total number of HTTP requests"
).unwrap();
let request_duration = Histogram::with_opts(
HistogramOpts::new(
"http_request_duration_seconds",
"HTTP request duration in seconds"
)
).unwrap();
[Link](Box::new(request_counter.clone())).unwrap();
[Link](Box::new(request_duration.clone())).unwrap();
Self {
registry: Arc::new(registry),
request_counter,
request_duration,
}
}
}
async fn metrics_middleware(
State(metrics): State<MetricsState>,
request: Request<axum::body::Body>,
next: Next,
) -> Response {
let start = Instant::now();
11 of 17 7/31/25, 12:55
Build REST APIs in Rust with Axum | Rustaceans about:reader?url=https%3A%2F%[Link]%2Frust...
metrics.request_counter.inc();
let response = [Link](request).await;
let duration = [Link]().as_secs_f64();
metrics.request_duration.observe(duration);
response
}
async fn metrics_handler(State(metrics): State<MetricsState>) ->
impl IntoResponse {
let encoder = TextEncoder::new();
let metric_families = [Link]();
match encoder.encode_to_string(&metric_families) {
Ok(output) => (StatusCode::OK, output),
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Failed to
encode metrics".to_string()),
}
}
async fn hello_world() -> &'static str {
"Welcome to the Task API with Metrics!"
}
async fn tasks_handler() -> &'static str {
"Tasks endpoint"
}
#[tokio::main]
async fn main() {
12 of 17 7/31/25, 12:55
Build REST APIs in Rust with Axum | Rustaceans about:reader?url=https%3A%2F%[Link]%2Frust...
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new("info,tower_http=debug"))
.with(tracing_subscriber::fmt::layer())
.init();
let metrics_state = MetricsState::new();
let app = Router::new()
.route("/", get(hello_world))
.route("/tasks", get(tasks_handler))
.route("/metrics", get(metrics_handler))
.layer(middleware::from_fn_with_state(
metrics_state.clone(),
metrics_middleware,
))
.layer(TraceLayer::new_for_http())
.with_state(metrics_state);
let listener = tokio::net::TcpListener::bind("[Link]:3000")
.await
.unwrap();
println!("Server running on [Link]
println!("Metrics available at [Link]
axum::serve(listener, app).[Link]();
}
This logs requests and exposes metrics at /metrics, critical for
production monitoring.
6. Robust Error Handling
13 of 17 7/31/25, 12:55
Build REST APIs in Rust with Axum | Rustaceans about:reader?url=https%3A%2F%[Link]%2Frust...
use axum::{http::StatusCode, response::{IntoResponse,
Response}, Json};
use serde::Serialize;
#[derive(Debug, Serialize)]
enum AppError {
InternalServerError,
NotFound,
InvalidInput(String),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, error_message) = match self {
AppError::InternalServerError =>
(StatusCode::INTERNAL_SERVER_ERROR, "Internal Server
Error".to_string()),
AppError::NotFound => (StatusCode::NOT_FOUND,
"Resource Not Found".to_string()),
AppError::InvalidInput(msg) => (StatusCode::BAD_REQUEST,
format!("Invalid Input: {}", msg)),
};
(status, Json(error_message)).into_response()
}
}
7. WebAssembly for Client-Side Validation
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn validate_task(title: &str) -> bool {
!title.is_empty() && [Link]() <= 100
}
14 of 17 7/31/25, 12:55
Build REST APIs in Rust with Axum | Rustaceans about:reader?url=https%3A%2F%[Link]%2Frust...
Compile and use in HTML:
cargo install wasm-pack
wasm-pack build --target web
<script type="module">
import init, { validate_task } from './pkg/task_validator.js';
async function run() {
await init();
[Link](validate_task("My Task")); // true
[Link](validate_task("")); // false
}
run();
</script>
This reduces server load by validating inputs client-side.
8. Testing the API
use axum::{routing::{get, post}, Router};
use axum_test::TestServer;
#[tokio::test]
async fn test_create_task() {
let app = Router::new().route("/tasks", post(create_task));
let server = TestServer::new(app).unwrap();
let response = server
.post("/tasks")
.json(&serde_json::json!({"title": "Test", "description": "Test
task"}))
.await;
assert_eq!(response.status_code(), StatusCode::CREATED);
}
Production Checklist
15 of 17 7/31/25, 12:55
Build REST APIs in Rust with Axum | Rustaceans about:reader?url=https%3A%2F%[Link]%2Frust...
• Structured Logging: Use tracing with ELK or Datadog
• Configuration: Manage secrets with dotenv or config crate
• Database: Use sqlx for async PostgreSQL queries
• Health Checks: Add GET /health returning 200 OK
• Graceful Shutdown:
use tokio::signal;
async fn shutdown_signal() {
signal::ctrl_c().[Link]("Failed to install Ctrl+C handler");
}
• Docker:
FROM rust:1.80 AS builder
WORKDIR /app
COPY . .
RUN cargo build
FROM debian:buster-slim
COPY
CMD ["task-api"]
• Monitoring: Expose Prometheus metrics at /metrics
• Security: Use JWT, HTTPS, and cargo audit
Common Pitfalls
• Forgetting Arc for shared state, causing ownership errors
• Not using Result for error propagation
• Blocking operations in async handlers (e.g., sync DB calls)
• Ignoring graceful shutdown, risking request loss
Conclusion
16 of 17 7/31/25, 12:55
Build REST APIs in Rust with Axum | Rustaceans about:reader?url=https%3A%2F%[Link]%2Frust...
Axum, powered by Rust’s safety and speed, is a game-changer for
production-ready REST APIs. With routing, extractors,
middleware, WebAssembly, and observability, you’re equipped to
build scalable, secure web services. The Rust community on X
praises Axum’s reliability in production, making it a top choice in
2025.
Ready to deploy your Axum API? Try the code, share your
project on X with #RustLang and #Axum, and join the Rust
community on Reddit or Discord.
What’s your experience with Axum? Share in the comments below!
17 of 17 7/31/25, 12:55