TechLead
Lesson 17 of 28
5 min read
Rust

Web Development with Actix Web

Build REST APIs with Actix Web in Rust: routing, extractors, middleware, JSON with serde, database integration, CORS, and error handling.

What is Actix Web?

Actix Web is one of the fastest web frameworks available in any language. Built on Rust's async ecosystem, it provides a powerful, pragmatic framework for building web services. It consistently tops the TechEmpower benchmarks.

Getting Started

// Cargo.toml
// [dependencies]
// actix-web = "4"
// serde = { version = "1", features = ["derive"] }
// serde_json = "1"
// tokio = { version = "1", features = ["full"] }

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

async fn hello() -> impl Responder {
    HttpResponse::Ok().body("Hello, World!")
}

async fn health() -> impl Responder {
    HttpResponse::Ok().json(serde_json::json!({
        "status": "healthy",
        "version": "1.0.0"
    }))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    println!("Server running at http://localhost:8080");

    HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(hello))
            .route("/health", web::get().to(health))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

Routing and Extractors

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

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

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

#[derive(Deserialize)]
struct Pagination {
    page: Option,
    per_page: Option,
}

// Path parameters
async fn get_user(path: web::Path) -> impl Responder {
    let user_id = path.into_inner();
    HttpResponse::Ok().json(User {
        id: user_id,
        name: "Alice".into(),
        email: "alice@example.com".into(),
    })
}

// JSON body
async fn create_user(body: web::Json) -> impl Responder {
    let user = User {
        id: 1,
        name: body.name.clone(),
        email: body.email.clone(),
    };
    HttpResponse::Created().json(user)
}

// Query parameters
async fn list_users(query: web::Query) -> impl Responder {
    let page = query.page.unwrap_or(1);
    let per_page = query.per_page.unwrap_or(20);
    HttpResponse::Ok().json(serde_json::json!({
        "page": page,
        "per_page": per_page,
        "users": []
    }))
}

// Configure routes
fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/api/v1")
            .route("/users", web::get().to(list_users))
            .route("/users", web::post().to(create_user))
            .route("/users/{id}", web::get().to(get_user))
    );
}

Middleware and Application State

use actix_web::{web, App, HttpServer, middleware};
use std::sync::Mutex;

// Application state
struct AppState {
    request_count: Mutex,
    db_pool: DatabasePool,
}

async fn with_state(data: web::Data) -> impl actix_web::Responder {
    let mut count = data.request_count.lock().unwrap();
    *count += 1;
    actix_web::HttpResponse::Ok().json(serde_json::json!({
        "request_number": *count
    }))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // Initialize shared state
    let state = web::Data::new(AppState {
        request_count: Mutex::new(0),
        db_pool: DatabasePool::new(),
    });

    HttpServer::new(move || {
        // CORS middleware
        let cors = actix_cors::Cors::default()
            .allow_any_origin()
            .allow_any_method()
            .allow_any_header();

        App::new()
            .app_data(state.clone())
            .wrap(cors)
            .wrap(middleware::Logger::default()) // Request logging
            .wrap(middleware::Compress::default()) // Gzip compression
            .route("/count", actix_web::web::get().to(with_state))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

struct DatabasePool;
impl DatabasePool { fn new() -> Self { DatabasePool } }

Error Handling

use actix_web::{HttpResponse, ResponseError};
use std::fmt;

#[derive(Debug)]
enum ApiError {
    NotFound(String),
    BadRequest(String),
    Internal(String),
}

impl fmt::Display for ApiError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ApiError::NotFound(msg) => write!(f, "Not Found: {msg}"),
            ApiError::BadRequest(msg) => write!(f, "Bad Request: {msg}"),
            ApiError::Internal(msg) => write!(f, "Internal Error: {msg}"),
        }
    }
}

impl ResponseError for ApiError {
    fn error_response(&self) -> HttpResponse {
        let (status, message) = match self {
            ApiError::NotFound(msg) => (
                actix_web::http::StatusCode::NOT_FOUND, msg),
            ApiError::BadRequest(msg) => (
                actix_web::http::StatusCode::BAD_REQUEST, msg),
            ApiError::Internal(msg) => (
                actix_web::http::StatusCode::INTERNAL_SERVER_ERROR, msg),
        };
        HttpResponse::build(status).json(serde_json::json!({
            "error": message
        }))
    }
}

async fn get_user_by_id(path: actix_web::web::Path) -> Result {
    let id = path.into_inner();
    if id == 0 {
        return Err(ApiError::BadRequest("ID must be > 0".into()));
    }
    if id > 1000 {
        return Err(ApiError::NotFound(format!("User {id} not found")));
    }
    Ok(HttpResponse::Ok().json(serde_json::json!({"id": id, "name": "Alice"})))
}

Key Takeaways

  • ✅ Actix Web is one of the fastest web frameworks, with excellent async support
  • ✅ Extractors (Path, Json, Query, Data) automatically parse request data
  • ✅ Application state is shared via web::Data with thread-safe wrappers
  • ✅ Implement ResponseError for custom error types with proper HTTP responses
  • ✅ Built-in middleware handles logging, compression, and CORS

Continue Learning