actix-web

Actix-web Framework Guide

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "actix-web" with this command: npx skills add ar4mirez/samuel/ar4mirez-samuel-actix-web

Actix-web Framework Guide

Applies to: Actix-web 4+, Rust Web APIs, High-Performance Services Complements: .claude/skills/rust-guide/SKILL.md

Core Principles

  • Type-Safe Extraction: Use extractors (web::Json , web::Path , web::Query ) for request data

  • Thin Handlers: Handlers delegate to services; no business logic in handlers

  • Structured Errors: Implement ResponseError for all error types; never return raw strings

  • Shared State via web::Data : Application state is injected, never global

  • Middleware for Cross-Cutting: Auth, logging, CORS belong in middleware, not handlers

Project Structure

myproject/ ├── Cargo.toml ├── src/ │ ├── main.rs # Entry point, server bootstrap │ ├── config.rs # Configuration loading │ ├── routes.rs # Route registration │ ├── handlers/ │ │ ├── mod.rs │ │ ├── users.rs # User-related handlers │ │ └── health.rs # Health check endpoint │ ├── models/ │ │ ├── mod.rs │ │ └── user.rs # Domain models + DTOs │ ├── services/ │ │ ├── mod.rs │ │ └── user_service.rs # Business logic layer │ ├── repositories/ │ │ ├── mod.rs │ │ └── user_repository.rs # Database access layer │ ├── middleware/ │ │ ├── mod.rs │ │ └── auth.rs # Authentication middleware │ └── errors/ │ ├── mod.rs │ └── app_error.rs # Centralized error types ├── tests/ │ └── integration_tests.rs └── migrations/

  • main.rs bootstraps server, loads config, creates pool, wires routes

  • Business logic lives in services/ , not in handlers

  • Database access isolated in repositories/

  • One error enum per crate with ResponseError impl

Dependencies (Cargo.toml)

[dependencies] actix-web = "4" actix-rt = "2" actix-cors = "0.7" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sqlx = { version = "0.7", features = ["runtime-tokio", "postgres", "uuid", "chrono"] } validator = { version = "0.16", features = ["derive"] } thiserror = "1.0" anyhow = "1.0" log = "0.4" env_logger = "0.10" tracing = "0.1" tracing-actix-web = "0.7" uuid = { version = "1.0", features = ["v4", "serde"] } chrono = { version = "0.4", features = ["serde"] }

[dev-dependencies] actix-rt = "2"

Application Entry Point

use actix_cors::Cors; use actix_web::{middleware, web, App, HttpServer}; use sqlx::postgres::PgPoolOptions;

pub struct AppState { pub pool: sqlx::PgPool, pub config: config::Config, }

#[actix_web::main] async fn main() -> std::io::Result<()> { dotenvy::dotenv().ok(); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

let config = config::Config::load().expect("Failed to load configuration");
let pool = PgPoolOptions::new()
    .max_connections(10)
    .connect(&#x26;config.database_url)
    .await
    .expect("Failed to create database pool");

sqlx::migrate!("./migrations")
    .run(&#x26;pool)
    .await
    .expect("Failed to run migrations");

let app_state = web::Data::new(AppState {
    pool,
    config: config.clone(),
});

HttpServer::new(move || {
    let cors = Cors::default()
        .allow_any_origin()
        .allow_any_method()
        .allow_any_header()
        .max_age(3600);

    App::new()
        .app_data(app_state.clone())
        .wrap(cors)
        .wrap(middleware::Logger::default())
        .wrap(middleware::Compress::default())
        .configure(routes::configure)
})
.bind(format!("{}:{}", config.host, config.port))?
.run()
.await

}

Routing

use actix_web::web;

pub fn configure(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/api/v1") .route("/health", web::get().to(handlers::health::health_check)) .service( web::scope("/auth") .route("/register", web::post().to(handlers::users::register)) .route("/login", web::post().to(handlers::users::login)), ) .service( web::scope("/users") .wrap(crate::middleware::auth::AuthMiddleware) .route("", web::get().to(handlers::users::list_users)) .route("/{id}", web::get().to(handlers::users::get_user)) .route("/{id}", web::put().to(handlers::users::update_user)) .route("/{id}", web::delete().to(handlers::users::delete_user)), ), ); }

Routing Guardrails

  • Group routes by resource under web::scope

  • Apply middleware at the scope level, not per-route

  • Use web::ServiceConfig for modular route registration

  • Version API routes (/api/v1/... )

  • Keep route definitions separate from handler implementations

Extractors

Extractors are how Actix-web injects request data into handler functions.

Extractor Purpose Example

web::Json<T>

Deserialize JSON body body: web::Json<CreateDto>

web::Path<T>

URL path parameters path: web::Path<Uuid>

web::Query<T>

Query string parameters query: web::Query<PaginationQuery>

web::Data<T>

Shared application state state: web::Data<AppState>

web::Form<T>

URL-encoded form data form: web::Form<LoginForm>

HttpRequest

Raw request (headers, extensions) req: HttpRequest

Extractor Rules

  • Always validate extracted data (use validator crate with Validate derive)

  • Prefer typed extractors over manually parsing HttpRequest

  • Use web::Data for immutable shared state (wrapped in Arc internally)

  • Limit JSON body size with .app_data(web::JsonConfig::default().limit(4096))

Handlers

Handlers are async functions that take extractors and return impl Responder .

use actix_web::{web, HttpResponse};

pub async fn register( state: web::Data<AppState>, body: web::Json<CreateUserDto>, ) -> AppResult<HttpResponse> { let service = UserService::new(state.pool.clone(), state.config.clone()); let response = service.register(body.into_inner()).await?; Ok(HttpResponse::Created().json(response)) }

pub async fn get_user( state: web::Data<AppState>, path: web::Path<Uuid>, ) -> AppResult<HttpResponse> { let service = UserService::new(state.pool.clone(), state.config.clone()); let user = service.get_user(path.into_inner()).await?; Ok(HttpResponse::Ok().json(user)) }

Handler Rules

  • Return Result<HttpResponse, AppError> (aliased as AppResult<HttpResponse> )

  • Use .into_inner() to unwrap extractors before passing to services

  • No business logic in handlers -- delegate to service layer

  • One handler per HTTP method per resource action

Application State

pub struct AppState { pub pool: sqlx::PgPool, pub config: Config, }

// Register in HttpServer closure: let app_state = web::Data::new(AppState { pool, config }); App::new().app_data(app_state.clone())

// Access in handlers: pub async fn handler(state: web::Data<AppState>) -> impl Responder { let pool = &state.pool; // use pool... }

State Rules

  • Wrap in web::Data (internally uses Arc )

  • Clone web::Data in HttpServer::new closure (cheap Arc clone)

  • Keep state immutable; use tokio::sync::RwLock if mutation is required

  • Do not store request-scoped data in AppState

Error Handling

use actix_web::{http::StatusCode, HttpResponse, ResponseError};

#[derive(Debug)] pub enum AppError { NotFound(String), BadRequest(String), Unauthorized(String), Forbidden(String), Conflict(String), Validation(String), Internal(String), Database(sqlx::Error), }

impl ResponseError for AppError { fn error_response(&self) -> HttpResponse { let (status, message) = match self { AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()), AppError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()), AppError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg.clone()), AppError::Forbidden(msg) => (StatusCode::FORBIDDEN, msg.clone()), AppError::Conflict(msg) => (StatusCode::CONFLICT, msg.clone()), AppError::Validation(msg) => (StatusCode::UNPROCESSABLE_ENTITY, msg.clone()), AppError::Internal() => (StatusCode::INTERNAL_SERVER_ERROR, "Internal error".into()), AppError::Database() => (StatusCode::INTERNAL_SERVER_ERROR, "Database error".into()), }; HttpResponse::build(status).json(serde_json::json!({ "success": false, "error": { "code": status.as_u16(), "message": message } })) } }

pub type AppResult<T> = Result<T, AppError>;

Error Handling Rules

  • Implement ResponseError trait for all custom error types

  • Use From<T> impls for automatic conversion: sqlx::Error , validator::ValidationErrors

  • Log internal/database errors server-side; return generic messages to clients

  • Define AppResult<T> type alias to reduce boilerplate

Middleware

Actix-web middleware uses the Transform

  • Service traits.

pub struct AuthMiddleware;

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

fn new_transform(&#x26;self, service: S) -> Self::Future {
    ok(AuthMiddlewareService { service: Rc::new(service) })
}

}

Middleware Rules

  • Use Transform trait for middleware factories, Service for middleware logic

  • Store authenticated user data in request extensions (req.extensions_mut().insert(...) )

  • Apply middleware at scope level: .service(web::scope("/protected").wrap(AuthMiddleware))

  • Use built-in middleware: Logger , Compress , NormalizePath , DefaultHeaders

  • Order matters: middleware wraps from bottom to top (last .wrap() runs first)

Configuration

use serde::Deserialize;

#[derive(Clone, Deserialize)] pub struct Config { pub host: String, pub port: u16, pub database_url: String, pub jwt_secret: String, pub jwt_expiration_hours: i64, }

impl Config { pub fn load() -> anyhow::Result<Self> { let config = config::Config::builder() .add_source(config::Environment::default().separator("__").try_parsing(true)) .set_default("host", "127.0.0.1")? .set_default("port", 8080)? .build()?; Ok(config.try_deserialize()?) } }

Commands

Development

cargo run # Start server cargo watch -x run # Watch mode (requires cargo-watch) RUST_LOG=actix_web=debug cargo run # Debug logging

Build

cargo build --release # Production build cargo check # Fast type-check

Quality

cargo fmt # Format code cargo clippy -- -D warnings # Lint with deny cargo test # Run all tests cargo tarpaulin # Coverage

Database

sqlx migrate run # Run migrations sqlx migrate add <name> # Create migration

Best Practices

Application Structure

  • Use web::Data for shared application state

  • Organize routes with web::scope and ServiceConfig

  • Service layer for business logic, repository layer for data access

  • Keep handlers thin: extract, delegate, respond

Performance

  • Use connection pooling (sqlx::PgPool or deadpool )

  • Enable middleware::Compress for response compression

  • Use async/await throughout; avoid blocking operations

  • Configure worker count for production: .workers(num_cpus::get())

  • Set JSON payload limits to prevent abuse

Security

  • Validate all inputs with validator crate

  • Use HTTPS in production (terminate at load balancer or use actix-tls )

  • Implement rate limiting (actix-governor or custom middleware)

  • Sanitize error messages: never expose internal details to clients

  • Use actix-cors with explicit origins in production (not allow_any_origin )

Testing

  • Use actix_web::test module for integration tests

  • Create test app with test::init_service and shared test state

  • Assert on status codes, response bodies, and headers

  • Test middleware independently from handlers

Advanced Topics

For detailed patterns and examples, see:

  • references/patterns.md -- Handler examples, database integration, authentication, actors, testing patterns

External References

  • Actix-web Documentation

  • Actix-web GitHub

  • Actix Examples

  • SQLx Documentation

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

frontend-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

blazor

No summary provided by upstream source.

Repository SourceNeeds Review
General

assembly-guide

No summary provided by upstream source.

Repository SourceNeeds Review
General

web-artifacts-builder

No summary provided by upstream source.

Repository SourceNeeds Review