--- name: actix-web description: | Actix Web is a powerful, high-performance web framework for Rust. It provides async handlers, type-safe extractors, middleware, and integrates with the Rust ecosystem for building fast, reliable, and memory-safe web services. license: Apache-2.0 compatibility: 'rust >= 1.72, cargo' metadata: author: terminal-skills version: 1.0.0 category: development tags: - rust - web - api - async - performance - type-safe --- # Actix Web Actix Web is one of the fastest web frameworks available. It uses Rust's type system for compile-time safety, async/await for concurrency, and extractors for ergonomic request handling. ## Installation ```toml # Cargo.toml — dependencies [dependencies] actix-web = "4" actix-rt = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" sqlx = { version = "0.7", features = ["runtime-tokio", "postgres", "macros"] } tokio = { version = "1", features = ["full"] } env_logger = "0.11" ``` ## Project Structure ``` # Recommended Actix Web project layout src/ ├── main.rs # Entry point and server config ├── config.rs # Configuration ├── routes/ │ ├── mod.rs # Route registration │ ├── articles.rs # Article handlers │ └── health.rs # Health check ├── models/ │ └── article.rs # Data structures ├── db/ │ └── article.rs # Database queries ├── middleware/ │ └── auth.rs # Auth middleware └── errors.rs # Error types ``` ## Main and Server Setup ```rust // src/main.rs — application entry point use actix_web::{web, App, HttpServer, middleware::Logger}; use sqlx::postgres::PgPoolOptions; mod routes; mod models; mod db; mod errors; #[actix_web::main] async fn main() -> std::io::Result<()> { env_logger::init(); let database_url = std::env::var("DATABASE_URL") .unwrap_or_else(|_| "postgres://localhost/mydb".to_string()); let pool = PgPoolOptions::new() .max_connections(10) .connect(&database_url) .await .expect("Failed to create pool"); HttpServer::new(move || { App::new() .wrap(Logger::default()) .app_data(web::Data::new(pool.clone())) .configure(routes::configure) }) .bind("0.0.0.0:8080")? .run() .await } ``` ## Route Configuration ```rust // src/routes/mod.rs — centralized route registration use actix_web::web; mod articles; mod health; pub fn configure(cfg: &mut web::ServiceConfig) { cfg.service( web::scope("/api") .route("/health", web::get().to(health::check)) .service( web::scope("/articles") .route("", web::get().to(articles::list)) .route("", web::post().to(articles::create)) .route("/{id}", web::get().to(articles::get)) .route("/{id}", web::delete().to(articles::delete)) ) ); } ``` ## Models ```rust // src/models/article.rs — data models with serde use serde::{Deserialize, Serialize}; use sqlx::FromRow; #[derive(Debug, Serialize, FromRow)] pub struct Article { pub id: i32, pub title: String, pub body: String, pub published: bool, pub created_at: chrono::NaiveDateTime, } #[derive(Debug, Deserialize)] pub struct CreateArticle { pub title: String, pub body: String, } #[derive(Debug, Deserialize)] pub struct ListParams { pub page: Option, pub limit: Option, } ``` ## Handlers with Extractors ```rust // src/routes/articles.rs — request handlers use actix_web::{web, HttpResponse}; use sqlx::PgPool; use crate::models::article::{Article, CreateArticle, ListParams}; use crate::errors::AppError; pub async fn list( pool: web::Data, query: web::Query, ) -> Result { let limit = query.limit.unwrap_or(20).min(100) as i64; let offset = ((query.page.unwrap_or(1) - 1) * limit as u32) as i64; let articles = sqlx::query_as::<_, Article>( "SELECT * FROM articles WHERE published = true ORDER BY created_at DESC LIMIT $1 OFFSET $2" ) .bind(limit) .bind(offset) .fetch_all(pool.get_ref()) .await?; Ok(HttpResponse::Ok().json(articles)) } pub async fn create( pool: web::Data, body: web::Json, ) -> Result { let article = sqlx::query_as::<_, Article>( "INSERT INTO articles (title, body) VALUES ($1, $2) RETURNING *" ) .bind(&body.title) .bind(&body.body) .fetch_one(pool.get_ref()) .await?; Ok(HttpResponse::Created().json(article)) } pub async fn get( pool: web::Data, path: web::Path, ) -> Result { let id = path.into_inner(); let article = sqlx::query_as::<_, Article>("SELECT * FROM articles WHERE id = $1") .bind(id) .fetch_optional(pool.get_ref()) .await? .ok_or(AppError::NotFound)?; Ok(HttpResponse::Ok().json(article)) } ``` ## Error Handling ```rust // src/errors.rs — custom error types use actix_web::{HttpResponse, ResponseError}; use std::fmt; #[derive(Debug)] pub enum AppError { NotFound, Internal(String), Database(sqlx::Error), } impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { AppError::NotFound => write!(f, "Not found"), AppError::Internal(msg) => write!(f, "{}", msg), AppError::Database(e) => write!(f, "Database error: {}", e), } } } impl ResponseError for AppError { fn error_response(&self) -> HttpResponse { match self { AppError::NotFound => HttpResponse::NotFound().json(serde_json::json!({"error": "not found"})), _ => HttpResponse::InternalServerError().json(serde_json::json!({"error": "internal error"})), } } } impl From for AppError { fn from(e: sqlx::Error) -> Self { AppError::Database(e) } } ``` ## Middleware ```rust // src/middleware/auth.rs — simple auth middleware use actix_web::{dev::ServiceRequest, Error, HttpMessage}; use actix_web::error::ErrorUnauthorized; pub async fn validate_token(req: &ServiceRequest) -> Result<(), Error> { let token = req.headers() .get("Authorization") .and_then(|v| v.to_str().ok()) .and_then(|v| v.strip_prefix("Bearer ")); match token { Some(t) => { let user_id = verify_jwt(t).map_err(|_| ErrorUnauthorized("invalid token"))?; req.extensions_mut().insert(user_id); Ok(()) } None => Err(ErrorUnauthorized("missing token")), } } ``` ## Testing ```rust // tests/articles_test.rs — integration test use actix_web::{test, App, web}; #[actix_web::test] async fn test_list_articles() { let pool = setup_test_db().await; let app = test::init_service( App::new() .app_data(web::Data::new(pool)) .configure(routes::configure) ).await; let req = test::TestRequest::get().uri("/api/articles").to_request(); let resp = test::call_service(&app, req).await; assert_eq!(resp.status(), 200); } ``` ## Key Patterns - Use extractors (`web::Json`, `web::Path`, `web::Query`, `web::Data`) for type-safe request parsing - Implement `ResponseError` on custom error types for automatic HTTP error responses - Use `web::Data` for shared application state (DB pool, config) — it's cheaply cloneable - Use `sqlx` with compile-time checked queries (`sqlx::query!`) for production code - Use `configure` functions to modularize route registration - Use `#[actix_web::test]` for async integration tests with `test::init_service`