Loading...
Loading...
Actix-web framework guardrails, patterns, and best practices for AI-assisted development. Use when working with Actix-web projects, or when the user mentions Actix-web. Provides actor patterns, async handlers, extractors, and high-performance web guidelines.
npx skill4agent add ar4mirez/samuel actix-webApplies to: Actix-web 4+, Rust Web APIs, High-Performance Services Complements:.claude/skills/rust-guide/SKILL.md
web::Jsonweb::Pathweb::QueryResponseErrorweb::Datamyproject/
├── 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.rsservices/repositories/ResponseError[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"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(&config.database_url)
.await
.expect("Failed to create database pool");
sqlx::migrate!("./migrations")
.run(&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
}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)),
),
);
}web::scopeweb::ServiceConfig/api/v1/...| Extractor | Purpose | Example |
|---|---|---|
| Deserialize JSON body | |
| URL path parameters | |
| Query string parameters | |
| Shared application state | |
| URL-encoded form data | |
| Raw request (headers, extensions) | |
validatorValidateHttpRequestweb::DataArc.app_data(web::JsonConfig::default().limit(4096))impl Responderuse 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))
}Result<HttpResponse, AppError>AppResult<HttpResponse>.into_inner()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...
}web::DataArcweb::DataHttpServer::newtokio::sync::RwLockAppStateuse 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>;ResponseErrorFrom<T>sqlx::Errorvalidator::ValidationErrorsAppResult<T>TransformServicepub 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(&self, service: S) -> Self::Future {
ok(AuthMiddlewareService { service: Rc::new(service) })
}
}TransformServicereq.extensions_mut().insert(...).service(web::scope("/protected").wrap(AuthMiddleware))LoggerCompressNormalizePathDefaultHeaders.wrap()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()?)
}
}# 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 migrationweb::Dataweb::scopeServiceConfigsqlx::PgPooldeadpoolmiddleware::Compress.workers(num_cpus::get())validatoractix-tlsactix-governoractix-corsallow_any_originactix_web::testtest::init_service