| name | actix-web |
| description | [Applies to: **/*.rs] Definitive guidelines for building high-performance, maintainable, and secure web applications with actix-web 4, focusing on modern Rust best practices. |
| source | cursor_mdc |
actix-web Best Practices
Actix-web 4 is the premier choice for high-performance Rust web services. Adhere to these guidelines for clean, scalable, and production-ready code. Always run cargo fmt and cargo clippy before committing.
1. Code Organization & Structure
Organize your codebase by feature or domain, not by generic file types. This improves discoverability and maintainability.
✅ GOOD: Modular by Feature
mod db;
mod routes;
mod services;
mod models;
use actix_web::{web, App, HttpServer};
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::default().default_filter_or("info"));
let db_pool = db::init_pool().await
.expect("Failed to create DB pool");
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(db_pool.clone()))
.service(
web::scope("/api/v1")
.configure(routes::users::config)
.configure(routes::products::config)
)
.wrap(actix_web::middleware::Logger::default())
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
use actix_web::{web, HttpResponse, Responder};
use crate::db::PgPool;
use crate::services::users as user_service;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(web::resource("/users").route(web::get().to(get_all_users)));
cfg.service(web::resource("/users/{id}").route(web::get().to(get_user_by_id)));
}
async fn get_all_users(pool: web::Data<PgPool>) -> impl Responder {
match user_service::find_all_users(&pool).await {
Ok(users) => HttpResponse::Ok().json(users),
Err(e) => {
log::error!("Failed to fetch users: {:?}", e);
HttpResponse::InternalServerError().finish()
},
}
}
use crate::db::PgPool;
use crate::models::User;
pub async fn find_all_users(pool: &PgPool) -> Result<Vec<User>, sqlx::Error> {
sqlx::query_as::<_, User>("SELECT id, name, email FROM users")
.fetch_all(pool)
.await
}
❌ BAD: Monolithic main.rs or types.rs/impl.rs for everything
This approach quickly becomes unmanageable and hinders collaboration.
2. Application State (web::Data<T>)
Use web::Data<T> for dependency injection and sharing application-wide state. For mutable state, wrap it in Arc<Mutex<T>> or Arc<RwLock<T>> and ensure it's initialized outside the HttpServer::new closure.
✅ GOOD: Shared Immutable and Mutable State
use actix_web::{web, App, HttpServer, Responder, HttpResponse};
use std::sync::{Arc, Mutex};
struct AppState {
app_name: String,
request_count: Arc<Mutex<usize>>,
}
async fn index(data: web::Data<AppState>) -> impl Responder {
let mut count = data.request_count.lock().unwrap();
*count += 1;
HttpResponse::Ok().body(format!(
"Hello from {}! Request count: {}",
data.app_name, count
))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let request_count = Arc::new(Mutex::new(0));
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(AppState {
app_name: "My Actix App".to_string(),
request_count: request_count.clone(),
}))
.route("/", web::get().to(index))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
❌ BAD: Mutable state initialized inside HttpServer::new
HttpServer::new(move || {
let request_count = Arc::new(Mutex::new(0));
App::new()
.app_data(web::Data::new(AppState {
app_name: "My Actix App".to_string(),
request_count: request_count.clone(),
}))
.route("/", web::get().to(index))
})
3. Extractors & Responders
Leverage Actix-web's powerful, type-safe extractors (web::Json, web::Path, web::Query) and ensure your handlers return types that implement Responder.
✅ GOOD: Type-Safe Request/Response Handling
use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct UserInfo {
name: String,
email: String,
}
#[derive(Serialize)]
struct UserResponse {
id: u32,
name: String,
}
async fn create_user(
path: web::Path<u32>,
user_info: web::Json<UserInfo>,
) -> impl Responder {
let user_id = path.into_inner();
log::info!("Creating user {} with name: {}", user_id, user_info.name);
HttpResponse::Created().json(UserResponse {
id: user_id,
name: user_info.name.clone(),
})
}
❌ BAD: Manual Request Body Parsing
async fn create_user_bad(mut payload: web::Payload) -> Result<HttpResponse, actix_web::Error> {
use futures::StreamExt;
let mut body = web::BytesMut::new();
while let Some(chunk) = payload.next().await {
body.extend_from_slice(&chunk?)
}
let user_info: UserInfo = serde_json::from_slice(&body)
.map_err(actix_web::error::ErrorBadRequest)?;
Ok(HttpResponse::Ok().finish())
}
4. Error Handling
Define custom error types that implement ResponseError for consistent, structured error responses. Use thiserror for ergonomic error definition.
✅ GOOD: Custom Error Types with ResponseError
use actix_web::{error::ResponseError, http::StatusCode, HttpResponse};
use derive_more::{Display, Error};
use serde::Serialize;
use crate::models::User;
use crate::db::PgPool;
#[derive(Debug, Display, Error, Serialize)]
#[display(fmt = "{}", message)]
pub enum AppError {
#[display(fmt = "User not found: {}", id)]
UserNotFound { id: u32, message: String },
#[display(fmt = "Database error: {}", _0)]
DbError(#[from] sqlx::Error),
#[display(fmt = "Validation error: {}", message)]
ValidationError { message: String },
#[display(fmt = "Internal server error")]
InternalError,
}
impl ResponseError for AppError {
fn error_response(&self) -> HttpResponse {
let status_code = match self {
AppError::UserNotFound { .. } => StatusCode::NOT_FOUND,
AppError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR,
AppError::ValidationError { .. } => StatusCode::BAD_REQUEST,
AppError::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
};
HttpResponse::build(status_code).json(self)
}
}
async fn get_user_handler(
path: web::Path<u32>,
pool: web::Data<PgPool>,
) -> Result<HttpResponse, AppError> {
let user_id = path.into_inner();
let user = sqlx::query_as::<_, User>("SELECT id, name, email FROM users WHERE id = $1")
.bind(user_id)
.fetch_optional(pool.get_ref())
.await?
.ok_or(AppError::UserNotFound { id: user_id, message: format!("User with ID {} not found", user_id) })?;
Ok(HttpResponse::Ok().json(user))
}
❌ BAD: Generic actix_web::Error or panic!
async fn get_user_bad(path: web::Path<u32>) -> Result<HttpResponse, actix_web::Error> {
let user_id = path.into_inner();
if user_id == 0 {
return Err(actix_web::error::ErrorBadRequest("Invalid ID"));
}
Ok(HttpResponse::Ok().finish())
}
async fn get_user_panic() -> impl Responder {
panic!("Something went terribly wrong!");
}
5. Middleware & Security
Employ actix-web's robust middleware for logging, compression, CORS, and other cross-cutting concerns. Always include Logger for request tracing.
✅ GOOD: Essential Middleware Stack
use actix_web::{middleware, App, HttpServer};
use actix_cors::Cors;
use env_logger::Env;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(Env::default().default_filter_or("info"));
HttpServer::new(move || {
let cors = Cors::default()
.allow_any_origin()
.allow_any_method()
.allow_any_header()
.max_age(3600);
App::new()
.wrap(middleware::Logger::default())
.wrap(middleware::Compress::default())
.wrap(middleware::NormalizePath::trim())
.wrap(cors)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
❌ BAD: Missing Logging or Basic Security
HttpServer::new(move || {
App::new()
})
6. Performance & Blocking Operations
Actix-web is asynchronous. Avoid blocking I/O or CPU-bound tasks directly in handlers. Use web::block for blocking operations or offload to a dedicated thread pool.
✅ GOOD: Offloading Blocking Operations
use actix_web::{web, HttpResponse, Responder};
use std::thread;
use std::time::Duration;
fn blocking_task(input: String) -> String {
thread::sleep(Duration::from_secs(1));
format!("Processed: {}", input)
}
async fn handle_blocking_request(body: web::Bytes) -> impl Responder {
let input = String::from_utf8(body.to_vec()).unwrap_or_default();
let result = web::block(move || blocking_task(input)).await;
match result {
Ok(output) => HttpResponse::Ok().body(output),
Err(e) => {