Actix-web

Highest-performing Rust web framework. Actor model-based with excellent concurrency, achieves top-class performance in benchmarks.

RustFrameworkWeb DevelopmentHigh PerformanceActorAsyncHTTP/2

GitHub Overview

actix/actix-web

Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.

Stars23,255
Watchers234
Forks1,756
Created:September 30, 2017
Language:Rust
License:Apache License 2.0

Topics

actixactix-webasyncrustwebweb-developmentwebsockets

Star History

actix/actix-web Star History
Data as of: 7/19/2025, 08:06 AM

Framework

Actix-web

Overview

Actix-web is a high-performance web framework developed in Rust. Based on the actor model, it combines asynchronous processing with memory safety.

Details

Actix-web was started in 2017 as a web framework for the Rust language. Initially built on the actix framework based on the actor model, it has now evolved to improve compatibility with Tokio. It consistently records top-level performance in the TechEmpower Framework Benchmark and is particularly fast among Rust web frameworks. It comes standard with full support for HTTP/1.x, HTTP/2, and WebSocket, a powerful routing system, flexible middleware architecture, and transparent content compression/decompression capabilities. It can safely handle request data through a type-safe extractor system and enables flexible response generation through the Responder trait. While providing enterprise-grade features such as rich authentication options, database integration, and template engine support, it maintains high performance through zero-cost abstractions.

Pros and Cons

Pros

  • Extremely High Performance: Top-class performance in TechEmpower benchmarks
  • Memory Safety: Compile-time error detection through Rust's type system
  • Asynchronous Processing: Efficient async I/O processing based on Tokio
  • Rich Features: HTTP/2, WebSocket, middleware, TLS support
  • Type Safety: Reduced runtime errors through strict compile-time type checking
  • Modular Design: Flexible architecture allowing selection of only needed features
  • Active Development: Continuous improvement and community support

Cons

  • Learning Cost: Requires understanding of Rust language and lifetime concepts
  • Compile Time: Long compilation times for Rust projects
  • Ecosystem: Relatively smaller library ecosystem compared to other languages
  • Memory Usage: Basic memory consumption due to rich features
  • Debug Complexity: Debugging difficulty of async code and lifetimes

Key Links

Code Examples

Hello World

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

#[get("/hello/{name}")]
async fn greet(name: web::Path<String>) -> impl Responder {
    format!("Hello {name}!")
}

#[actix_web::main] // or #[tokio::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().service(greet)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

Routing and Extractors

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

#[derive(Deserialize)]
struct Info {
    name: String,
    age: u32,
}

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

// Path parameters
async fn get_user(path: web::Path<u32>) -> Result<impl Responder> {
    let user_id = path.into_inner();
    let user = User {
        id: user_id,
        name: format!("User{}", user_id),
        email: format!("user{}@example.com", user_id),
    };
    
    Ok(HttpResponse::Ok().json(user))
}

// Query parameters
async fn search_users(query: web::Query<Info>) -> Result<impl Responder> {
    Ok(HttpResponse::Ok().json(format!(
        "Searching for name: {}, age: {}",
        query.name, query.age
    )))
}

// JSON body
async fn create_user(user: web::Json<Info>) -> Result<impl Responder> {
    println!("Creating new user: {:?}", user);
    
    let new_user = User {
        id: 1,
        name: user.name.clone(),
        email: format!("{}@example.com", user.name),
    };
    
    Ok(HttpResponse::Created().json(new_user))
}

// Form data
async fn update_user(
    path: web::Path<u32>,
    form: web::Form<Info>
) -> Result<impl Responder> {
    let user_id = path.into_inner();
    
    Ok(HttpResponse::Ok().json(format!(
        "Updating user {}: name={}, age={}",
        user_id, form.name, form.age
    )))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/users/{id}", web::get().to(get_user))
            .route("/users/search", web::get().to(search_users))
            .route("/users", web::post().to(create_user))
            .route("/users/{id}", web::put().to(update_user))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

Using Middleware

use actix_web::{
    dev::{ServiceRequest, ServiceResponse, Service, Transform},
    web, App, HttpServer, HttpResponse, Result, Error,
    middleware::{Logger, NormalizePath, DefaultHeaders, Compress},
    http::header,
};
use futures_util::future::LocalBoxFuture;
use std::future::{ready, Ready};
use std::rc::Rc;

// Custom middleware
pub struct Authentication;

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

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

pub struct AuthenticationMiddleware<S> {
    service: Rc<S>,
}

impl<S, B> Service<ServiceRequest> for AuthenticationMiddleware<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    actix_web::dev::forward_ready!(service);

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let service = self.service.clone();

        Box::pin(async move {
            // Check authentication header
            let auth_header = req
                .headers()
                .get("Authorization")
                .and_then(|h| h.to_str().ok());

            if let Some(token) = auth_header {
                if token == "Bearer valid_token" {
                    // Authentication successful, continue request
                    let res = service.call(req).await?;
                    Ok(res)
                } else {
                    // Invalid token
                    let (req, _) = req.into_parts();
                    let res = HttpResponse::Unauthorized()
                        .json("Invalid authentication token")
                        .map_into_right_body();
                    Ok(ServiceResponse::new(req, res))
                }
            } else {
                // No authentication header
                let (req, _) = req.into_parts();
                let res = HttpResponse::Unauthorized()
                    .json("Authentication required")
                    .map_into_right_body();
                Ok(ServiceResponse::new(req, res))
            }
        })
    }
}

async fn protected_resource() -> Result<HttpResponse> {
    Ok(HttpResponse::Ok().json("Accessing protected resource"))
}

async fn public_resource() -> Result<HttpResponse> {
    Ok(HttpResponse::Ok().json("Public resource"))
}

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

    HttpServer::new(|| {
        App::new()
            // Global middleware
            .wrap(Logger::default())
            .wrap(NormalizePath::trim())
            .wrap(DefaultHeaders::new().add(("X-Version", "1.0")))
            .wrap(Compress::default())
            
            // Public resources
            .route("/public", web::get().to(public_resource))
            
            // Protected resources (with authentication middleware)
            .service(
                web::scope("/api")
                    .wrap(Authentication)
                    .route("/protected", web::get().to(protected_resource))
            )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

Database Integration and Testing

use actix_web::{web, App, HttpServer, HttpResponse, Result, middleware::Logger};
use sqlx::{PgPool, Pool, Postgres, Row};
use serde::{Deserialize, Serialize};

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

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

// Get all users from database
async fn get_users(pool: web::Data<PgPool>) -> Result<HttpResponse> {
    let result = sqlx::query("SELECT id, name, email FROM users")
        .fetch_all(pool.get_ref())
        .await;

    match result {
        Ok(rows) => {
            let users: Vec<User> = rows
                .iter()
                .map(|row| User {
                    id: row.get("id"),
                    name: row.get("name"),
                    email: row.get("email"),
                })
                .collect();
            Ok(HttpResponse::Ok().json(users))
        }
        Err(e) => {
            eprintln!("Database error: {}", e);
            Ok(HttpResponse::InternalServerError().json("Database error"))
        }
    }
}

// Create new user
async fn create_user(
    pool: web::Data<PgPool>,
    user: web::Json<CreateUser>,
) -> Result<HttpResponse> {
    let result = sqlx::query(
        "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email"
    )
    .bind(&user.name)
    .bind(&user.email)
    .fetch_one(pool.get_ref())
    .await;

    match result {
        Ok(row) => {
            let new_user = User {
                id: row.get("id"),
                name: row.get("name"),
                email: row.get("email"),
            };
            Ok(HttpResponse::Created().json(new_user))
        }
        Err(e) => {
            eprintln!("User creation error: {}", e);
            Ok(HttpResponse::BadRequest().json("Failed to create user"))
        }
    }
}

// Get specific user
async fn get_user(
    pool: web::Data<PgPool>,
    path: web::Path<i32>
) -> Result<HttpResponse> {
    let user_id = path.into_inner();
    
    let result = sqlx::query("SELECT id, name, email FROM users WHERE id = $1")
        .bind(user_id)
        .fetch_optional(pool.get_ref())
        .await;

    match result {
        Ok(Some(row)) => {
            let user = User {
                id: row.get("id"),
                name: row.get("name"),
                email: row.get("email"),
            };
            Ok(HttpResponse::Ok().json(user))
        }
        Ok(None) => {
            Ok(HttpResponse::NotFound().json("User not found"))
        }
        Err(e) => {
            eprintln!("Database error: {}", e);
            Ok(HttpResponse::InternalServerError().json("Database error"))
        }
    }
}

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

    // Create database connection pool
    let database_url = std::env::var("DATABASE_URL")
        .unwrap_or_else(|_| "postgres://user:password@localhost/mydb".to_string());
    
    let pool = PgPool::connect(&database_url)
        .await
        .expect("Failed to connect to database");

    // Create table (use migrations in real applications)
    sqlx::query(
        r#"
        CREATE TABLE IF NOT EXISTS users (
            id SERIAL PRIMARY KEY,
            name VARCHAR NOT NULL,
            email VARCHAR UNIQUE NOT NULL
        )
        "#
    )
    .execute(&pool)
    .await
    .expect("Failed to create table");

    HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(pool.clone()))
            .wrap(Logger::default())
            .route("/users", web::get().to(get_users))
            .route("/users", web::post().to(create_user))
            .route("/users/{id}", web::get().to(get_user))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

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

    #[actix_web::test]
    async fn test_get_users() {
        // Create test database pool
        let pool = PgPool::connect("sqlite::memory:")
            .await
            .expect("Failed to create test database");

        let app = test::init_service(
            App::new()
                .app_data(web::Data::new(pool))
                .route("/users", web::get().to(get_users))
        ).await;

        let req = test::TestRequest::get()
            .uri("/users")
            .to_request();
        
        let resp = test::call_service(&app, req).await;
        assert!(resp.status().is_success());
    }
}

WebSocket and File Upload

use actix_web::{
    web, App, HttpServer, HttpResponse, Result, Error,
    middleware::Logger,
};
use actix_files as fs;
use actix_multipart::Multipart;
use actix_web_actors::ws;
use futures_util::StreamExt as _;
use std::io::Write;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;

// WebSocket actor
struct MyWebSocket;

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

impl actix::StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWebSocket {
    fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
        match msg {
            Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
            Ok(ws::Message::Text(text)) => {
                println!("Received: {}", text);
                ctx.text(format!("Echo: {}", text))
            }
            Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
            Ok(ws::Message::Close(reason)) => {
                ctx.close(reason);
                ctx.stop();
            }
            _ => (),
        }
    }
}

// WebSocket handler
async fn websocket(req: actix_web::HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
    ws::start(MyWebSocket, &req, stream)
}

// File upload handler
async fn upload_file(mut payload: Multipart) -> Result<HttpResponse> {
    let mut uploaded_files = Vec::new();

    while let Some(item) = payload.next().await {
        let mut field = item?;
        
        let content_disposition = field.content_disposition();
        
        if let Some(filename) = content_disposition.get_filename() {
            let filepath = format!("./uploads/{}", sanitize_filename::sanitize(&filename));
            
            // Create file
            let mut file = File::create(&filepath).await?;
            
            // Write file chunk by chunk
            while let Some(chunk) = field.next().await {
                let data = chunk?;
                file.write_all(&data).await?;
            }
            
            uploaded_files.push(filename.to_string());
        }
    }

    if uploaded_files.is_empty() {
        Ok(HttpResponse::BadRequest().json("No files were uploaded"))
    } else {
        Ok(HttpResponse::Ok().json(serde_json::json!({
            "message": "File upload successful",
            "files": uploaded_files
        })))
    }
}

// Get file list
async fn list_files() -> Result<HttpResponse> {
    let mut files = Vec::new();
    
    if let Ok(mut entries) = tokio::fs::read_dir("./uploads").await {
        while let Some(entry) = entries.next_entry().await? {
            if let Some(filename) = entry.file_name().to_str() {
                files.push(filename.to_string());
            }
        }
    }
    
    Ok(HttpResponse::Ok().json(files))
}

// Root handler
async fn index() -> Result<HttpResponse> {
    Ok(HttpResponse::Ok().content_type("text/html").body(r#"
    <!DOCTYPE html>
    <html>
    <head>
        <title>Actix-web Demo</title>
        <meta charset="utf-8">
    </head>
    <body>
        <h1>Actix-web WebSocket and File Upload Demo</h1>
        
        <h2>WebSocket Test</h2>
        <div>
            <input type="text" id="messageInput" placeholder="Enter message">
            <button onclick="sendMessage()">Send</button>
        </div>
        <div id="messages"></div>
        
        <h2>File Upload</h2>
        <form action="/upload" method="post" enctype="multipart/form-data">
            <input type="file" name="file" multiple>
            <button type="submit">Upload</button>
        </form>
        
        <h2>Uploaded Files</h2>
        <div id="fileList"></div>
        
        <script>
            // WebSocket connection
            const ws = new WebSocket('ws://127.0.0.1:8080/ws');
            const messages = document.getElementById('messages');
            
            ws.onmessage = function(event) {
                const div = document.createElement('div');
                div.textContent = event.data;
                messages.appendChild(div);
            };
            
            function sendMessage() {
                const input = document.getElementById('messageInput');
                ws.send(input.value);
                input.value = '';
            }
            
            // Get file list
            fetch('/files')
                .then(response => response.json())
                .then(files => {
                    const fileList = document.getElementById('fileList');
                    files.forEach(file => {
                        const div = document.createElement('div');
                        div.innerHTML = `<a href="/static/${file}" target="_blank">${file}</a>`;
                        fileList.appendChild(div);
                    });
                });
        </script>
    </body>
    </html>
    "#))
}

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

    // Create uploads directory
    tokio::fs::create_dir_all("./uploads").await?;

    HttpServer::new(|| {
        App::new()
            .wrap(Logger::default())
            .route("/", web::get().to(index))
            .route("/ws", web::get().to(websocket))
            .route("/upload", web::post().to(upload_file))
            .route("/files", web::get().to(list_files))
            .service(fs::Files::new("/static", "./uploads").show_files_listing())
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

Error Handling and Response Management

use actix_web::{
    web, App, HttpServer, HttpResponse, Result, ResponseError,
    middleware::{ErrorHandlerResponse, ErrorHandlers},
    http::{StatusCode, header::ContentType},
    dev::ServiceResponse,
};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};

#[derive(Debug, Serialize)]
struct ErrorResponse {
    error: String,
    message: String,
    code: u16,
}

// Custom error type
#[derive(Debug)]
enum AppError {
    ValidationError(String),
    DatabaseError(String),
    NotFoundError(String),
    AuthenticationError,
}

impl Display for AppError {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        match self {
            AppError::ValidationError(msg) => write!(f, "Validation error: {}", msg),
            AppError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
            AppError::NotFoundError(msg) => write!(f, "Not found: {}", msg),
            AppError::AuthenticationError => write!(f, "Authentication error"),
        }
    }
}

impl ResponseError for AppError {
    fn status_code(&self) -> StatusCode {
        match self {
            AppError::ValidationError(_) => StatusCode::BAD_REQUEST,
            AppError::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
            AppError::NotFoundError(_) => StatusCode::NOT_FOUND,
            AppError::AuthenticationError => StatusCode::UNAUTHORIZED,
        }
    }

    fn error_response(&self) -> HttpResponse {
        let error_response = ErrorResponse {
            error: self.to_string(),
            message: match self {
                AppError::ValidationError(msg) => msg.clone(),
                AppError::DatabaseError(_) => "Internal server error occurred".to_string(),
                AppError::NotFoundError(msg) => msg.clone(),
                AppError::AuthenticationError => "Authentication required".to_string(),
            },
            code: self.status_code().as_u16(),
        };

        HttpResponse::build(self.status_code())
            .insert_header(ContentType::json())
            .json(error_response)
    }
}

#[derive(Deserialize)]
struct UserInput {
    name: String,
    email: String,
    age: u32,
}

// Handler with validation
async fn create_user_with_validation(user: web::Json<UserInput>) -> Result<HttpResponse, AppError> {
    // Validation
    if user.name.is_empty() {
        return Err(AppError::ValidationError("Name is required".to_string()));
    }
    
    if user.age < 18 {
        return Err(AppError::ValidationError("Must be 18 or older".to_string()));
    }
    
    if !user.email.contains('@') {
        return Err(AppError::ValidationError("Please enter a valid email address".to_string()));
    }

    // Simulate database operation
    if user.email == "[email protected]" {
        return Err(AppError::DatabaseError("Database connection error".to_string()));
    }

    // Success response
    Ok(HttpResponse::Created().json(serde_json::json!({
        "message": "User created successfully",
        "user": {
            "name": user.name,
            "email": user.email,
            "age": user.age
        }
    })))
}

// Handler requiring authentication
async fn protected_endpoint(req: actix_web::HttpRequest) -> Result<HttpResponse, AppError> {
    let auth_header = req
        .headers()
        .get("Authorization")
        .and_then(|h| h.to_str().ok());

    match auth_header {
        Some(token) if token.starts_with("Bearer ") => {
            if token == "Bearer valid_token" {
                Ok(HttpResponse::Ok().json(serde_json::json!({
                    "message": "Successfully accessed protected endpoint",
                    "data": "Confidential data"
                })))
            } else {
                Err(AppError::AuthenticationError)
            }
        }
        _ => Err(AppError::AuthenticationError),
    }
}

// Handler to find user
async fn find_user(path: web::Path<u32>) -> Result<HttpResponse, AppError> {
    let user_id = path.into_inner();
    
    if user_id == 999 {
        return Err(AppError::NotFoundError("User not found".to_string()));
    }
    
    Ok(HttpResponse::Ok().json(serde_json::json!({
        "id": user_id,
        "name": format!("User{}", user_id),
        "email": format!("user{}@example.com", user_id)
    })))
}

// Global error handlers
fn json_error_handler() -> ErrorHandlers<actix_web::body::BoxBody> {
    ErrorHandlers::new()
        .handler(StatusCode::NOT_FOUND, not_found_handler)
        .handler(StatusCode::INTERNAL_SERVER_ERROR, internal_error_handler)
}

fn not_found_handler<B>(res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>, Error> {
    let (req, _) = res.into_parts();
    
    let error_response = ErrorResponse {
        error: "Not Found".to_string(),
        message: "The requested resource was not found".to_string(),
        code: 404,
    };
    
    let res = HttpResponse::NotFound()
        .json(error_response)
        .map_into_right_body();
    
    Ok(ErrorHandlerResponse::Response(ServiceResponse::new(req, res)))
}

fn internal_error_handler<B>(res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>, Error> {
    let (req, _) = res.into_parts();
    
    let error_response = ErrorResponse {
        error: "Internal Server Error".to_string(),
        message: "An internal server error occurred".to_string(),
        code: 500,
    };
    
    let res = HttpResponse::InternalServerError()
        .json(error_response)
        .map_into_right_body();
    
    Ok(ErrorHandlerResponse::Response(ServiceResponse::new(req, res)))
}

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

    HttpServer::new(|| {
        App::new()
            .wrap(actix_web::middleware::Logger::default())
            .wrap(json_error_handler())
            .route("/users", web::post().to(create_user_with_validation))
            .route("/users/{id}", web::get().to(find_user))
            .route("/protected", web::get().to(protected_endpoint))
            .route("/", web::get().to(|| async {
                HttpResponse::Ok().json(serde_json::json!({
                    "message": "Actix-web Error Handling Demo",
                    "endpoints": [
                        "POST /users - Create user (with validation)",
                        "GET /users/{id} - Get user (404 error with 999)",
                        "GET /protected - Protected endpoint (requires Bearer token)"
                    ]
                }))
            }))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}