$ npx rulesync-cli pull✓ Wrote CLAUDE.md (2 rulesets)# Coding Standards- Always use async/await- Prefer named exports
Rule Writing

CLAUDE.md for Actix Web (Rust)

Actix Web is Rust's fastest web framework — AI generates patterns that fight the borrow checker and ignore extractors. Rules for app state, extractors, guards, and error handling.

7 min read·December 8, 2025

Actix Web is Rust's fastest framework — AI fights its ownership model

web::Data state, typed extractors, ServiceConfig routing, and ResponseError patterns

Why Actix Web Needs Framework-Specific Rules

Actix Web is Rust's highest-performance web framework — consistently topping the TechEmpower benchmarks. But its ownership-aware API confuses AI assistants: shared state requires Arc<Mutex<T>> or web::Data<T>, extractors need correct ordering in handler signatures, and error handling requires implementing ResponseError. AI generates Express/Gin patterns that don't compile in Rust.

The most common AI failures: trying to share mutable state without proper synchronization (compiler rejects it), wrong extractor ordering in handler parameters, manual JSON parsing instead of using serde extractors, panic-based error handling instead of Result + ResponseError, and creating one giant configure function instead of modular ServiceConfig.

These rules layer on top of Rust language rules. Actix Web has its own idioms on top of Rust's — extractors, app data, guards, middleware, and the configure pattern for modular routing.

Rule 1: App State with web::Data

The rule: 'Use web::Data<T> for shared application state: database pools, configuration, caches. Register with app_data: App::new().app_data(web::Data::new(pool.clone())). Access in handlers as an extractor: async fn get_user(db: web::Data<Pool>) -> impl Responder. For mutable shared state, use web::Data<Mutex<T>> or web::Data<RwLock<T>> — Actix requires Send + Sync for all state.'

For the database pool: 'Create the pool at startup: let pool = PgPool::connect(&database_url).await?. Wrap in web::Data: let pool = web::Data::new(pool). Pass to App: App::new().app_data(pool.clone()). Clone the web::Data (it's an Arc internally) — each worker thread gets a reference. Never create database connections per request.'

AI tries to use global state, pass state through closures, or create new connections per handler. web::Data is Actix's answer to dependency injection — it's safe, concurrent, and the only correct way to share state across handlers.

  • web::Data<T> for shared state: DB pools, config, caches
  • web::Data<Mutex<T>> for mutable shared state
  • Register with .app_data() — access as extractor in handler params
  • Clone web::Data (Arc internally) for each App instance
  • Never global state, per-request connections, or closure-captured mutability
💡 web::Data Is DI

web::Data<T> is Actix's dependency injection — safe, concurrent, Arc-based. Register once with app_data(), access everywhere as an extractor. Never global state, never per-request connections, never closure captures.

Rule 2: Typed Extractors for Request Data

The rule: 'Use typed extractors for all request data. web::Path<T> for path params: async fn get_user(path: web::Path<(u64,)>). web::Query<T> for query params: async fn search(query: web::Query<SearchParams>). web::Json<T> for JSON body: async fn create(body: web::Json<CreateUser>). All extractors use serde for deserialization and validate types automatically.'

For custom extractors: 'Create FromRequest implementations for complex extraction logic (auth tokens, pagination, custom headers). Custom extractors can return errors that map to HTTP responses. Use them like built-in extractors in handler signatures.'

For extractor configuration: 'Configure JSON payload limits: web::JsonConfig::default().limit(4096). Configure path error handling: web::PathConfig::default().error_handler(|err, req| { ... }). Configure at the App or scope level — not per route.'

Rule 3: Modular Routing with ServiceConfig

The rule: 'Use ServiceConfig functions for modular route registration: fn user_routes(cfg: &mut web::ServiceConfig) { cfg.service(web::resource("/users").route(web::get().to(list_users)).route(web::post().to(create_user))); }. Register in main: App::new().configure(user_routes).configure(order_routes). Each domain has its own configure function in a separate file.'

For route organization: 'Group related routes with web::scope: web::scope("/api/v1").configure(user_routes).configure(order_routes). Apply middleware to scopes: web::scope("/admin").wrap(AdminGuard). Use web::resource for CRUD: web::resource("/users/{id}").route(web::get().to(get)).route(web::put().to(update)).route(web::delete().to(delete)).'

AI puts all routes in main() as a flat list. ServiceConfig functions are Actix's module system — each configure function is testable, reusable, and independently maintainable. Use them like Express routers or Gin route groups.

ℹ️ ServiceConfig = Modules

ServiceConfig functions are Actix's module system. Each domain gets a configure fn in its own file: user_routes(cfg), order_routes(cfg). Compose in main: App::new().configure(users).configure(orders). Testable and independent.

Rule 4: Error Handling with ResponseError

The rule: 'Define custom error types that implement actix_web::ResponseError: #[derive(Debug, thiserror::Error)] enum AppError { #[error("Not found")] NotFound, #[error("Validation failed: {0}")] Validation(String), #[error("Internal error")] Internal(#[from] anyhow::Error) }. Implement ResponseError to map errors to HTTP responses: fn error_response(&self) -> HttpResponse and fn status_code(&self) -> StatusCode.'

For handler returns: 'Handlers return Result<impl Responder, AppError>. Use ? for error propagation: let user = db.get_user(id).await.map_err(|_| AppError::NotFound)?;. This is idiomatic Rust error handling adapted for HTTP — errors propagate with ? and convert to HTTP responses via ResponseError.'

AI generates panic!() or unwrap() in handlers — crashing the worker thread on any error. Result + ResponseError is the correct Actix pattern: errors become HTTP responses, not crashes.

  • Custom error enum with thiserror — implements ResponseError
  • status_code() maps error variants to HTTP status codes
  • error_response() creates the HTTP response body
  • ? operator for propagation — map_err for conversion
  • Never unwrap() or panic!() in handlers — Result<T, AppError> always
⚠️ Never unwrap() in Handlers

unwrap() panics on error — crashing the Actix worker thread. Result<T, AppError> with ? propagation converts errors to HTTP responses via ResponseError. The handler stays alive, the user gets a proper error.

Rule 5: Middleware and Guards

The rule: 'Use .wrap() for middleware on App or scope: App::new().wrap(Logger::default()).wrap(Cors::default()). Use guards for route-level conditions: web::get().guard(guard::Header("content-type", "application/json")). Custom middleware implements Transform + Service traits — use actix-web-lab's from_fn for simpler middleware: .wrap(from_fn(my_middleware)).'

For common middleware: 'actix-web Logger for request logging, actix-cors for CORS, actix-web-lab for utilities. For auth middleware, create a middleware that extracts the token, validates it, inserts the user into request extensions, and calls next. Access user in handlers: req.extensions().get::<User>().'

For testing: 'Use actix_web::test for handler testing: let app = test::init_service(App::new().configure(user_routes)).await. Send test requests: let resp = test::call_service(&app, req).await. Assert status and body: assert_eq!(resp.status(), StatusCode::OK). No server needed — test the full middleware + handler pipeline in-process.'

Complete Actix Web Rules Template

Consolidated rules for Actix Web projects (use alongside Rust language rules).

  • web::Data<T> for shared state — never global, never per-request connections
  • Typed extractors: web::Path, web::Query, web::Json — serde for all deserialization
  • ServiceConfig functions for modular routing — one configure fn per domain
  • web::scope for grouped routes + scoped middleware — web::resource for CRUD
  • Custom AppError + ResponseError — Result<T, AppError> on all handlers — never unwrap()
  • .wrap() for middleware — guards for route conditions — from_fn for simple middleware
  • actix_web::test for in-process testing — no server needed
  • cargo clippy + cargo test in CI — actix-cors + Logger as baseline middleware