Building a Modern API with Axum, Tokio using Rust Programming Language

Published 2026-03-30 15:28:24 · 35 views

Introduction

In recent years, Rust has gained massive popularity for backend development due to its performance, safety, and concurrency guarantees. Among the web frameworks in Rust, Axum stands out for its simplicity, type safety, and strong ecosystem integration.

In this article, we’ll explore how to build a clean, production-ready API using Axum 0.8., and why it’s becoming a preferred choice for modern backend systems.


Why Choose Axum?

Axum is built on top of:

  • hyper (HTTP engine)

  • tower (middleware and service abstraction)

Key Advantages

  • Type-safe request handling

  • Composable routing

  • Built-in async support

  • Excellent performance

  • Seamless integration with the Rust ecosystem


Project Setup

First, create a new Rust project:

cargo new axum-api
cd axum-api

Add dependencies in Cargo.toml:

[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"


Basic API Structure

Here’s a minimal API server:

use axum::{
    routing::{get, post},
    Json, Router,
};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;

#[derive(Serialize)]
struct HealthResponse {
    status: String,
}

async fn health_check() -> Json<HealthResponse> {
    Json(HealthResponse {
        status: "ok".to_string(),
    })
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/health", get(health_check));

    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!("Server running on {}", addr);

    axum::serve(tokio::net::TcpListener::bind(addr).await.unwrap(), app)
        .await
        .unwrap();
}

Request Handling with JSON

Define request and response models:

#[derive(Deserialize)]
struct CreateUser {
    name: String,
}

#[derive(Serialize)]
struct UserResponse {
    id: u64,
    name: String,
}

Handler:

async fn create_user(Json(payload): Json<CreateUser>) -> Json<UserResponse> {
    Json(UserResponse {
        id: 1,
        name: payload.name,
    })
}

Add route:

.route("/users", post(create_user))

Extractors: The Power of Axum

Axum uses extractors to parse incoming requests:

  • Json<T> → JSON body

  • Path<T> → URL params

  • Query<T> → query string

  • State<T> → shared app state

Example:

use axum::extract::Path;

async fn get_user(Path(id): Path<u64>) -> String {
    format!("User ID: {}", id)
}

Application State

In production, you’ll need shared state like DB connections.

use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    app_name: String,
}

let state = Arc::new(AppState {
    app_name: "My API".into(),
});

let app = Router::new()
    .route("/", get(handler))
    .with_state(state);

Middleware (Tower Layer)

Axum leverages tower for middleware:

use tower_http::trace::TraceLayer;

let app = Router::new()
    .route("/", get(handler))
    .layer(TraceLayer::new_for_http());

Common middleware:

  • Logging

  • Authentication

  • Rate limiting

  • CORS


Error Handling

Custom error handling improves API quality:

use axum::{http::StatusCode, response::IntoResponse};

async fn handler() -> Result<String, StatusCode> {
    Err(StatusCode::NOT_FOUND)
}

You can also define custom error types by implementing IntoResponse.


Production Best Practices

1. Structured Project Layout

src/
 ├── main.rs
 ├── routes/
 ├── handlers/
 ├── models/
 ├── services/
 └── state/

2. Use Environment Config

Use dotenv or config crate for:

  • DB URLs

  • API keys

  • environment configs


3. Logging & Observability

Use:

  • tracing

  • tower-http


4. Database Integration

Common choices:

  • sqlx (async, compile-time checked)

  • diesel (ORM)


5. Validation

Use crates like:

  • validator

  • serde custom validation


Performance & Scalability

Axum benefits from:

  • Rust’s zero-cost abstractions

  • async runtime (tokio)

  • no GC pauses

👉 Ideal for:

  • high-throughput APIs

  • microservices

  • real-time systems


When to Use Axum

Choose Axum if you want:

  • Strong type safety

  • High performance

  • Control over architecture

  • Modern async Rust stack


Conclusion

Axum 0.8. provides a clean, powerful, and scalable way to build APIs in Rust. While it has a learning curve (especially around ownership and async), the payoff is a highly reliable and performant system.

If you’re building modern backend systems and care about safety + performance, Axum is a strong choice.