تخطَّ إلى المحتوى

Actix Web Cheat Sheet

Overview

Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust. Built on top of the Actix actor system and Tokio async runtime, it consistently ranks at the top of web framework benchmarks. Actix Web uses Rust’s type system to provide compile-time guarantees about request handling, making it virtually impossible to have runtime type errors in route handlers.

The framework provides type-safe request extractors, middleware support, WebSocket handling, and HTTP/2 support out of the box. Its handler functions use async/await syntax and the extractor pattern, where request data is automatically deserialized and validated based on function parameter types. Actix Web is used in production by companies requiring high throughput and reliability.

Installation

Setup

# Create new project
cargo new my-api
cd my-api

Cargo.toml

[package]
name = "my-api"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4"
actix-rt = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
env_logger = "0.11"
log = "0.4"

Minimal Server

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

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init();

    HttpServer::new(|| {
        App::new()
            .wrap(middleware::Logger::default())
            .route("/", web::get().to(|| async {
                HttpResponse::Ok().json(serde_json::json!({"message": "Hello, World!"}))
            }))
    })
    .bind("0.0.0.0:8080")?
    .run()
    .await
}

Routing

Route Registration

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

fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/api")
            .route("/users", web::get().to(get_users))
            .route("/users", web::post().to(create_user))
            .route("/users/{id}", web::get().to(get_user))
            .route("/users/{id}", web::put().to(update_user))
            .route("/users/{id}", web::delete().to(delete_user))
    );
}

// Using macro attributes
#[actix_web::get("/health")]
async fn health() -> HttpResponse {
    HttpResponse::Ok().json(serde_json::json!({"status": "ok"}))
}

#[actix_web::post("/users")]
async fn create_user(body: web::Json<CreateUser>) -> HttpResponse {
    HttpResponse::Created().json(body.into_inner())
}

// Register in App
App::new()
    .configure(config)
    .service(health)
    .service(create_user)

Path Parameters

use actix_web::{web, HttpResponse};

// Single parameter
#[actix_web::get("/users/{id}")]
async fn get_user(path: web::Path<String>) -> HttpResponse {
    let id = path.into_inner();
    HttpResponse::Ok().json(serde_json::json!({"id": id}))
}

// Multiple parameters
#[actix_web::get("/users/{user_id}/posts/{post_id}")]
async fn get_post(path: web::Path<(String, String)>) -> HttpResponse {
    let (user_id, post_id) = path.into_inner();
    HttpResponse::Ok().json(serde_json::json!({
        "user_id": user_id,
        "post_id": post_id
    }))
}

// Typed path extraction
#[derive(serde::Deserialize)]
struct PostPath {
    user_id: u64,
    post_id: u64,
}

#[actix_web::get("/users/{user_id}/posts/{post_id}")]
async fn get_post_typed(path: web::Path<PostPath>) -> HttpResponse {
    HttpResponse::Ok().json(serde_json::json!({
        "user_id": path.user_id,
        "post_id": path.post_id
    }))
}

Scopes and Guards

use actix_web::{web, guard, App};

App::new()
    .service(
        web::scope("/api/v1")
            .guard(guard::Header("Accept", "application/json"))
            .route("/users", web::get().to(get_users))
    )
    .service(
        web::scope("/admin")
            .wrap(auth_middleware)
            .route("/dashboard", web::get().to(dashboard))
    )
    .default_service(web::route().to(not_found))

Extractors

Common Extractors

ExtractorSourceExample
web::Path<T>URL path parametersweb::Path<(String, u32)>
web::Query<T>Query stringweb::Query<SearchParams>
web::Json<T>JSON request bodyweb::Json<CreateUser>
web::Form<T>Form dataweb::Form<LoginForm>
web::Data<T>Application stateweb::Data<AppState>
HttpRequestFull requestDirect access

Query Parameters

#[derive(serde::Deserialize)]
struct SearchParams {
    q: String,
    #[serde(default = "default_page")]
    page: u32,
    #[serde(default = "default_limit")]
    limit: u32,
}

fn default_page() -> u32 { 1 }
fn default_limit() -> u32 { 20 }

#[actix_web::get("/search")]
async fn search(query: web::Query<SearchParams>) -> HttpResponse {
    HttpResponse::Ok().json(serde_json::json!({
        "query": query.q,
        "page": query.page,
        "limit": query.limit
    }))
}

JSON Body

#[derive(serde::Deserialize, serde::Serialize)]
struct CreateUser {
    name: String,
    email: String,
    #[serde(default)]
    age: Option<u32>,
}

#[actix_web::post("/users")]
async fn create_user(
    body: web::Json<CreateUser>,
    db: web::Data<DbPool>,
) -> actix_web::Result<HttpResponse> {
    let user = body.into_inner();
    let result = db.insert_user(&user).await
        .map_err(actix_web::error::ErrorInternalServerError)?;
    Ok(HttpResponse::Created().json(result))
}

Application State

struct AppState {
    db: DbPool,
    config: AppConfig,
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let state = web::Data::new(AppState {
        db: create_pool().await,
        config: load_config(),
    });

    HttpServer::new(move || {
        App::new()
            .app_data(state.clone())
            .service(get_users)
    })
    .bind("0.0.0.0:8080")?
    .run()
    .await
}

#[actix_web::get("/users")]
async fn get_users(state: web::Data<AppState>) -> HttpResponse {
    let users = state.db.get_users().await;
    HttpResponse::Ok().json(users)
}

Middleware

Built-in Middleware

use actix_web::middleware;

App::new()
    .wrap(middleware::Logger::default())
    .wrap(middleware::Compress::default())
    .wrap(middleware::NormalizePath::trim())
    .wrap(middleware::DefaultHeaders::new()
        .add(("X-Version", "1.0"))
        .add(("X-Content-Type-Options", "nosniff")))

Custom Middleware

use actix_web::{dev::{Service, ServiceRequest, ServiceResponse, Transform}, Error};
use std::future::{Future, Ready, ready};
use std::pin::Pin;
use std::time::Instant;

pub struct Timer;

impl<S, B> Transform<S, ServiceRequest> for Timer
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Transform = TimerMiddleware<S>;
    type InitError = ();
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(TimerMiddleware { service }))
    }
}

pub struct TimerMiddleware<S> {
    service: S,
}

impl<S, B> Service<ServiceRequest> for TimerMiddleware<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;

    fn poll_ready(&self, cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
        self.service.poll_ready(cx)
    }

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let start = Instant::now();
        let fut = self.service.call(req);
        Box::pin(async move {
            let mut res = fut.await?;
            let elapsed = start.elapsed();
            res.headers_mut().insert(
                actix_web::http::header::HeaderName::from_static("x-response-time"),
                actix_web::http::header::HeaderValue::from_str(
                    &format!("{}ms", elapsed.as_millis())
                ).unwrap(),
            );
            Ok(res)
        })
    }
}

Configuration

Server Configuration

HttpServer::new(|| {
    App::new()
        .service(index)
})
.bind("0.0.0.0:8080")?
.workers(4)                    // Number of worker threads
.max_connections(25_000)       // Max concurrent connections
.max_connection_rate(256)      // Max connection rate
.keep_alive(std::time::Duration::from_secs(75))
.client_request_timeout(std::time::Duration::from_secs(5))
.shutdown_timeout(30)          // Graceful shutdown timeout
.run()
.await

JSON Configuration

// Custom JSON config
let json_cfg = web::JsonConfig::default()
    .limit(4096)  // Max payload size
    .error_handler(|err, _req| {
        let detail = err.to_string();
        actix_web::error::InternalError::from_response(
            err,
            HttpResponse::BadRequest().json(serde_json::json!({
                "error": "Invalid JSON",
                "detail": detail
            })),
        ).into()
    });

App::new()
    .app_data(json_cfg)

Advanced Usage

Error Handling

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

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

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::NotFound(msg) => write!(f, "Not found: {}", msg),
            AppError::BadRequest(msg) => write!(f, "Bad request: {}", msg),
            AppError::Internal(msg) => write!(f, "Internal error: {}", msg),
        }
    }
}

impl ResponseError for AppError {
    fn error_response(&self) -> HttpResponse {
        match self {
            AppError::NotFound(msg) =>
                HttpResponse::NotFound().json(serde_json::json!({"error": msg})),
            AppError::BadRequest(msg) =>
                HttpResponse::BadRequest().json(serde_json::json!({"error": msg})),
            AppError::Internal(msg) =>
                HttpResponse::InternalServerError().json(serde_json::json!({"error": msg})),
        }
    }
}

WebSocket

use actix_web::{web, HttpRequest, HttpResponse};
use actix_web_actors::ws;

struct MyWs;

impl actix::Actor for MyWs {
    type Context = ws::WebsocketContext<Self>;
}

impl actix::StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
    fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
        match msg {
            Ok(ws::Message::Text(text)) => ctx.text(text),
            Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
            Ok(ws::Message::Close(reason)) => ctx.close(reason),
            _ => (),
        }
    }
}

#[actix_web::get("/ws")]
async fn websocket(req: HttpRequest, stream: web::Payload) -> HttpResponse {
    ws::start(MyWs {}, &req, stream).unwrap()
}

Testing

#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::{test, App};

    #[actix_web::test]
    async fn test_health() {
        let app = test::init_service(
            App::new().service(health)
        ).await;

        let req = test::TestRequest::get().uri("/health").to_request();
        let resp = test::call_service(&app, req).await;
        assert_eq!(resp.status(), 200);

        let body: serde_json::Value = test::read_body_json(resp).await;
        assert_eq!(body["status"], "ok");
    }

    #[actix_web::test]
    async fn test_create_user() {
        let app = test::init_service(
            App::new().service(create_user)
        ).await;

        let user = serde_json::json!({"name": "Alice", "email": "a@b.com"});
        let req = test::TestRequest::post()
            .uri("/users")
            .set_json(&user)
            .to_request();
        let resp = test::call_service(&app, req).await;
        assert_eq!(resp.status(), 201);
    }
}

Troubleshooting

ProblemSolution
App data not configuredEnsure app_data() called before routes; check Data<T> type matches
Extractor type mismatchVerify struct derives Deserialize; check JSON field names
Borrow checker errorsUse web::Data<T> for shared state; clone when needed
Connection refusedCheck bind address; ensure port is available
Slow compilationUse cargo-watch for dev; split into workspace
Memory usage highCheck for unbounded channels; tune worker count
PayloadErrorIncrease JsonConfig limit; check Content-Type header
Middleware orderMiddleware wraps in reverse order of .wrap() calls