Skip to main content
โšก Calmops

Rust Web Development 2026 Complete Guide: Actix, Axum, and Modern Rust

Introduction

Rust has emerged as a top choice for building high-performance web services. Known for its memory safety and zero-cost abstractions, Rust enables developers to create web applications that are both fast and safe. In 2026, the Rust web ecosystem has matured significantly, with production-ready frameworks and excellent tooling.

This guide covers Rust web development in 2026, from choosing a framework to deploying production services. Whether you’re building APIs or full-stack applications, this guide provides practical insights for modern Rust web development.

The Rust Web Ecosystem

Why Rust for Web?

Rust offers compelling advantages for web development:

  • Performance: Near C-level speed, ideal for high-throughput services
  • Memory Safety: No null pointers, no buffer overflows
  • Concurrency: Fearless parallelism with async/await
  • Tooling: Excellent package manager (cargo), great IDE support

Key Frameworks

Framework Focus Performance Maturity
Axum Ergonomics Excellent High
Actix-web Performance Best High
Rocket Developer Experience Good High
Warp Composable Excellent Medium

Axum Framework

Getting Started

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

#[derive(Debug, Serialize, Deserialize)]
struct User {
    id: i32,
    name: String,
    email: String,
}

async fn get_users() -> Vec<User> {
    vec![
        User { id: 1, name: "Alice".into(), email: "[email protected]".into() },
        User { id: 2, name: "Bob".into(), email: "[email protected]".into() },
    ]
}

async fn create_user(Json(user): Json<CreateUserRequest>) -> Json<User> {
    // Create user in database
    Json(User {
        id: 3,
        name: user.name,
        email: user.email,
    })
}

#[derive(Debug, Deserialize)]
struct CreateUserRequest {
    name: String,
    email: String,
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/users", get(get_users).post(create_user))
        .route("/health", get(|| async { "OK" }));

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

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

Middleware

use axum::{
    extract::Request,
    middleware::Next,
    response::Response,
    Router,
};
use std::time::Instant;

async fn timing_middleware(
    request: Request,
    next: Next,
) -> Response {
    let start = Instant::now();
    
    let response = next.run(request).await;
    
    let duration = start.elapsed();
    println!("Request took {}ms", duration.as_millis());
    
    response
}

let app = Router::new()
    .route("/api/users", get(get_users))
    .layer(axum::middleware::from_fn(timing_middleware));

State Management

use axum::{
    extract::State,
    Router,
};
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;

#[derive(Clone)]
struct AppState {
    users: Arc<RwLock<HashMap<i32, User>>>,
    db: Arc<DbPool>,
}

async fn get_user(
    State(state): State<AppState>,
    Path(user_id): Path<i32>,
) -> Result<Json<User>, AppError> {
    let users = state.users.read().await;
    
    users.get(&user_id)
        .cloned()
        .map(Json)
        .ok_or(AppError::NotFound)
}

let state = AppState {
    users: Arc::new(RwLock::new(HashMap::new())),
    db: pool,
};

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

Actix-web

High-Performance Server

use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Item {
    id: i32,
    name: String,
}

async fn get_items() -> impl Responder {
    web::block(|| {
        // Blocking database call in thread pool
        vec![
            Item { id: 1, name: "Item 1".into() },
            Item { id: 2, name: "Item 2".into() },
        ]
    })
    .await
    .map(|items| HttpResponse::Ok().json(items))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/items", web::get().to(get_items))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

WebSocket Support

use actix_web::{web, App, HttpServer, HttpResponse, Error};
use actix_ws::{Session, Message};
use std::sync::Arc;
use tokio::sync::broadcast;

async fn ws_handler(
    req: actix_web::HttpRequest,
    stream: actix_web::web::Payload,
) -> Result<HttpResponse, Error> {
    let (response, session, msg_stream) = actix_ws::handle(&req, stream)?;
    
    tokio::spawn(async move {
        let mut session = session;
        
        while let Some(msg) = msg_stream.next().await {
            match msg {
                Ok(Message::Text(text)) => {
                    session.text(text).await?;
                }
                Ok(Message::Close(_)) => break,
                _ => {}
            }
        }
        
        session.close(None).await.ok();
    });
    
    Ok(response)
}

Database Integration

SQLx

use sqlx::{postgres::PgPoolOptions, Row, PgRow};

#[derive(Debug)]
struct User {
    id: i32,
    name: String,
    email: String,
}

impl From<PgRow> for User {
    fn from(row: PgRow) -> Self {
        User {
            id: row.get("id"),
            name: row.get("name"),
            email: row.get("email"),
        }
    }
}

async fn get_users(pool: &PgPool) -> Result<Vec<User>, sqlx::Error> {
    let users = sqlx::query_as!(
        User,
        "SELECT id, name, email FROM users WHERE active = true"
    )
    .fetch_all(pool)
    .await?;
    
    Ok(users)
}

// With parameters
async fn get_user_by_id(pool: &PgPool, id: i32) -> Result<Option<User>, sqlx::Error> {
    let user = sqlx::query_as!(
        User,
        "SELECT id, name, email FROM users WHERE id = $1",
        id
    )
    .fetch_optional(pool)
    .await?;
    
    Ok(user)
}

SeaORM

use sea_orm::{EntityTrait, QueryFilter, ActiveModel};

#[derive(Clone)]
struct DbConnection {
    conn: sea_orm::DbConn,
}

impl DbConnection {
    async fn create_user(&self, name: String, email: String) -> Result<User, sea_orm::DbErr> {
        let active_model = user::ActiveModel {
            name: Set(name),
            email: Set(email),
            ..Default::default()
        };
        
        User::insert(active_model)
            .exec(&self.conn)
            .await
            .map(|insert_result| {
                User {
                    id: insert_result.last_insert_id,
                    name,
                    email,
                }
            })
    }
    
    async fn find_user(&self, id: i32) -> Result<Option<User>, sea_orm::DbErr> {
        User::find_by_id(id)
            .one(&self.conn)
            .await
    }
}

Authentication

JWT Auth

use jsonwebtoken::{encode, decode, Header, Validation, EncodingKey, DecodingKey};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    sub: String,
    exp: usize,
}

const SECRET: &[u8] = b"your_secret_key";

fn create_token(user_id: &str) -> Result<String, jsonwebtoken::errors::Error> {
    let expiration = chrono::Utc::now()
        .checked_add_signed(chrono::Duration::hours(24))
        .unwrap()
        .timestamp();
    
    let claims = Claims {
        sub: user_id.to_string(),
        exp: expiration,
    };
    
    encode(&Header::default(), &claims, &EncodingKey::from_secret(SECRET))
}

fn validate_token(token: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
    let token_data = decode::<Claims>(
        token,
        &DecodingKey::from_secret(SECRET),
        &Validation::default(),
    )?;
    
    Ok(token_data.claims)
}

// Axum middleware
async fn auth_middleware(
    headers: HeaderMap,
    next: Next,
) -> Response {
    match headers.get("Authorization") {
        Some(auth_header) => {
            let token = auth_header.to_str().unwrap_or("");
            
            match validate_token(token) {
                Ok(claims) => {
                    // Add user ID to extensions
                    let mut req = request.into_request();
                    req.extensions_mut().insert(claims.sub);
                    next.run(req).await
                }
                Err(_) => HttpResponse::Unauthorized().into(),
            }
        }
        None => HttpResponse::Unauthorized().into(),
    }
}

Error Handling

use axum::{
    response::{IntoResponse, Response},
    Json,
};
use serde_json::json;
use thiserror::Error;

#[derive(Error, Debug)]
pub enum AppError {
    #[error("User not found")]
    NotFound,
    
    #[error("Invalid input: {0}")]
    BadRequest(String),
    
    #[error("Database error: {0}")]
    DatabaseError(String),
    
    #[error("Unauthorized")]
    Unauthorized,
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, error_message) = match self {
            AppError::NotFound => (StatusCode::NOT_FOUND, "Not found"),
            AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, &msg),
            AppError::DatabaseError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, &msg),
            AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized"),
        };
        
        let body = Json(json!({
            "error": error_message,
        }));
        
        (status, body).into_response()
    }
}

Testing

Unit Tests

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_create_user_request() {
        let request = CreateUserRequest {
            name: "Alice".into(),
            email: "[email protected]".into(),
        };
        
        assert_eq!(request.name, "Alice");
        assert!(request.email.contains("@"));
    }
    
    #[test]
    fn test_token_generation() {
        let token = create_token("user123").unwrap();
        assert!(!token.is_empty());
        
        let claims = validate_token(&token).unwrap();
        assert_eq!(claims.sub, "user123");
    }
}

Integration Tests

#[cfg(test)]
mod integration_tests {
    use super::*;
    use axum::{
        body::Body,
        routing::get,
        Router,
    };
    
    #[tokio::test]
    async fn test_health_endpoint() {
        let app = Router::new()
            .route("/health", get(|| async { "OK" }));
        
        let response = app
            .oneshot(
                Request::builder()
                    .uri("/health")
                    .body(Body::empty())
                    .unwrap()
            )
            .await
            .unwrap();
        
        assert_eq!(response.status(), StatusCode::OK);
    }
}

Deployment

Docker

# Build stage
FROM rust:1.75 as builder

WORKDIR /app
COPY . .
RUN cargo build --release

# Runtime stage
FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y \
    libssl3 \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY --from=builder /app/target/release/myapp .
COPY --from=builder /app/public ./public

EXPOSE 3000

CMD ["./myapp"]

Kubernetes

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rust-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: rust-api
  template:
    metadata:
      labels:
        app: rust-api
    spec:
      containers:
      - name: api
        image: myapp:latest
        ports:
        - containerPort: 3000
        resources:
          limits:
            memory: "512Mi"
            cpu: "500m"
          requests:
            memory: "256Mi"
            cpu: "250m"

External Resources

Documentation

Learning

Community

Conclusion

Rust web development in 2026 offers excellent frameworks, great tooling, and outstanding performance. Whether you choose Axum for its ergonomics or Actix-web for maximum speed, you’ll build reliable, high-performance services.

Start with Axum if you want productivity. Choose Actix-web if raw performance is critical. Either way, embrace Rust’s safety guarantees and build robust web services.

The ecosystem continues to grow. Stay engaged with the community and keep learning.

Comments