Skip to main content
Rust for Backend Engineers

Web Frameworks Compared

Ravinder··6 min read
RustBackendAxumActixRocketWeb Frameworks
Share:
Web Frameworks Compared

The Rust web framework ecosystem has consolidated around three serious contenders: Axum, Actix-web, and Rocket. All three can handle production traffic. They differ in design philosophy, ergonomics, and where the complexity lives. Choosing the wrong one for your team adds friction that compounds over months.

This post compares them through practical code, not benchmarks.

The Landscape

quadrantChart title Rust Web Framework Tradeoffs x-axis "More Magic / Convention" --> "More Explicit / Control" y-axis "Simpler API" --> "More Powerful API" quadrant-1 Powerful and Explicit quadrant-2 Powerful with Convention quadrant-3 Simple with Convention quadrant-4 Simple and Explicit Axum: [0.75, 0.72] Actix-web: [0.80, 0.85] Rocket: [0.25, 0.55]

Axum and Actix-web target engineers who want control. Rocket targets developers who want to get something running quickly with minimal boilerplate.

Axum: The Tower-Native Choice

Axum is maintained by the Tokio team. It builds directly on tower (the service/middleware abstraction) and hyper. This means any tower middleware works with Axum without adaptation.

use axum::{
    extract::{Path, State},
    routing::{get, post},
    Json, Router,
};
use sqlx::PgPool;
 
#[derive(Clone)]
struct AppState {
    pool: PgPool,
}
 
async fn get_user(
    Path(id): Path<u64>,
    State(state): State<AppState>,
) -> Result<Json<User>, UserError> {
    let user = fetch_user(id, &state.pool).await?;
    Ok(Json(user))
}
 
async fn create_user(
    State(state): State<AppState>,
    Json(body): Json<CreateUserRequest>,
) -> Result<Json<User>, UserError> {
    let user = insert_user(body, &state.pool).await?;
    Ok(Json(user))
}
 
fn build_router(pool: PgPool) -> Router {
    let state = AppState { pool };
    Router::new()
        .route("/users/:id", get(get_user))
        .route("/users", post(create_user))
        .with_state(state)
}

Axum's extractors (Path, State, Json, Query, Headers) are composable and compile-time checked. Incorrect extractor combinations are compile errors, not runtime panics.

Adding middleware uses the tower Layer API:

use tower_http::{cors::CorsLayer, trace::TraceLayer};
 
fn build_router(pool: PgPool) -> Router {
    build_router(pool)
        .layer(TraceLayer::new_for_http())
        .layer(CorsLayer::permissive())
}

Axum is the right default for teams already using the Tokio ecosystem. Its documentation is excellent, its error messages are clear, and it does not hide complexity.

Actix-web: The Actor Runtime

Actix-web is built on the Actix actor framework and runs its own executor. It was famously the fastest web framework in the TechEmpower benchmarks for years, though the gap has narrowed.

use actix_web::{get, post, web, App, HttpServer, Responder, HttpResponse};
use serde::{Deserialize, Serialize};
 
#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
}
 
#[derive(Deserialize)]
struct CreateUser {
    name: String,
}
 
#[get("/users/{id}")]
async fn get_user(path: web::Path<u64>) -> impl Responder {
    let id = path.into_inner();
    let user = User { id, name: format!("user-{}", id) };
    HttpResponse::Ok().json(user)
}
 
#[post("/users")]
async fn create_user(body: web::Json<CreateUser>) -> impl Responder {
    let user = User { id: 1, name: body.name.clone() };
    HttpResponse::Created().json(user)
}
 
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(get_user)
            .service(create_user)
    })
    .bind("0.0.0.0:3000")?
    .run()
    .await
}

Actix-web's macro-based routing (#[get], #[post]) is ergonomic. The main friction point is interoperability with non-Actix ecosystem code. If you need a library that only works with Tokio-native futures, you may hit impedance.

Actix-web is the right choice when raw performance and the actor model genuinely align with your architecture, or when your team is already invested in Actix.

Rocket: Convention Over Configuration

Rocket prioritizes developer ergonomics and fast onboarding. It uses Rust's procedural macro system heavily to let you express routes declaratively.

#[macro_use] extern crate rocket;
use rocket::serde::json::Json;
use serde::{Deserialize, Serialize};
 
#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
}
 
#[derive(Deserialize)]
struct CreateUser {
    name: String,
}
 
#[get("/users/<id>")]
fn get_user(id: u64) -> Json<User> {
    Json(User { id, name: format!("user-{}", id) })
}
 
#[post("/users", data = "<body>")]
fn create_user(body: Json<CreateUser>) -> Json<User> {
    Json(User { id: 1, name: body.name.clone() })
}
 
#[launch]
fn rocket() -> _ {
    rocket::build()
        .mount("/", routes![get_user, create_user])
}

Rocket's appeal is legibility. Route parameters, guards, and response types read almost like documentation. The tradeoff is that when you need to escape the convention — custom middleware, non-standard auth, dynamic routing — you encounter the framework's internals, which are harder to reason about than Axum's explicit layer system.

Rocket runs on Tokio as of Rocket 0.5 and beyond, which resolved the previous ecosystem isolation issue.

Side-by-Side Decision Points

flowchart TD A[Choosing a Rust web framework] --> B{Does your team value Tower ecosystem compatibility?} B -- Yes --> C[Axum] B -- No --> D{Priority: raw performance + actor model?} D -- Yes --> E[Actix-web] D -- No --> F{Priority: fastest onboarding / convention?} F -- Yes --> G[Rocket] F -- No --> C
Concern Axum Actix-web Rocket
Tower middleware Native Adapter needed Limited
Onboarding speed Medium Medium Fast
Ecosystem breadth High (Tokio) Medium Medium
Compile error quality Excellent Good Good
Macro reliance Low Low High
Docs quality Excellent Good Good

Shared Patterns Regardless of Framework

All three frameworks handle JSON, query params, path params, headers, and state injection. The routing ergonomics differ; the underlying patterns do not.

// Middleware concept common to all: trace every request
// Axum: tower_http::trace::TraceLayer
// Actix: actix_web::middleware::Logger
// Rocket: Fairing trait
 
// State injection common to all:
// Axum: State extractor
// Actix: web::Data
// Rocket: managed state via .manage()

For new services in 2025, the default recommendation is Axum. The Tower ecosystem is the broadest, the Tokio team maintains it, and the compile-time extractor checking catches a category of bugs that runtime frameworks miss entirely.

Key Takeaways

  • Axum integrates natively with the Tower middleware ecosystem, making it the natural choice for Tokio-first teams building services that need flexible, composable middleware.
  • Actix-web offers the highest raw throughput ceiling and suits teams already invested in the Actix actor model.
  • Rocket's macro-driven API has the gentlest onboarding curve but introduces more framework magic that becomes friction at the edges.
  • All three run on Tokio as of their current stable versions — the executor isolation that previously separated Actix is largely resolved.
  • Compile-time extractor checking in Axum catches a class of handler bugs (wrong argument order, missing state) before they reach production.
  • For most new backend services in Rust, start with Axum unless you have a specific reason to prefer one of the alternatives.
Share: