Skip to content
Back to skills

Rust Web

ASecurity

Expert skill for Rust web development with Axum and Actix Web. Covers Tower ecosystem, extractors, middleware, state management, Tokio runtime, Serde, error handling (thiserror, anyhow, IntoResponse), database integration (SQLx, Diesel, SeaORM), testing, and deployment. WHEN: \"Axum\", \"Actix Web\", \"Actix web\", \"actix-web\", \"Tower service\", \"Tower layer\", \"Rust web\", \"Rust API\", \"Rust REST\", \"Tokio runtime\", \"FromRequest\", \"FromRequestParts\", \"IntoResponse\", \"Response...

  • 4 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 24, 2026
testingrustgosqldockertestingapidatabaseperformance

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned September 24, 2026

npx -y skills add chrishuffman5/domain-expert --skill rust-web --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Rust Web?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Rust Web
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/chrishuffman5-rust-web/badge)](https://www.skillsdirectory.com/skills/chrishuffman5-rust-web)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: rust-web
description: "Expert skill for Rust web development with Axum and Actix Web. Covers Tower ecosystem, extractors, middleware, state management, Tokio runtime, Serde, error handling (thiserror, anyhow, IntoResponse), database integration (SQLx, Diesel, SeaORM), testing, and deployment. WHEN: \"Axum\", \"Actix Web\", \"Actix web\", \"actix-web\", \"Tower service\", \"Tower layer\", \"Rust web\", \"Rust API\", \"Rust REST\", \"Tokio runtime\", \"FromRequest\", \"FromRequestParts\", \"IntoResponse\", \"ResponseError\", \"AppError\", \"extractors\", \"web::Data\", \"axum::extract\", \"tower-http\", \"SQLx\", \"Diesel\", \"SeaORM\", \"thiserror\", \"Rust middleware\", \"Rust handler\", \"spawn_blocking\", \"JoinSet\"."
license: MIT
---

# Rust Web Frameworks

This skill covers Rust web development with Axum (Tower ecosystem) and Actix Web (actor system). Both frameworks run on the Tokio async runtime and deliver outstanding performance with compile-time safety guarantees. Rust's ownership model eliminates data races at compile time, and the type system enforces correct error handling through `Result<T, E>`.

## How to Approach Tasks

1. **Classify** the request:
   - **Architecture** -- Load `references/architecture.md` for Tower Service/Layer traits, Axum router and handler system, extractors (FromRequest/FromRequestParts), Actix App builder and actor model, Tokio runtime, Serde derive macros, error handling patterns
   - **Best practices** -- Load `references/best-practices.md` for project structure, error handling (AppError + IntoResponse), testing, database patterns, state management, deployment, performance tuning, common crates
   - **Framework selection** -- Use the decision guide below
   - **Troubleshooting** -- Load both reference files; common issues involve lifetime errors, `Send + Sync` bounds, and extractor ordering

2. **Identify framework** -- Determine from imports (`axum::`, `actix_web::`, `tower::`, `actix::`) or explicit mention. Default to Axum for new projects unless actor-model requirements exist.

3. **Load context** -- Read the relevant reference file before answering.

4. **Analyze** -- Apply Rust-specific reasoning. Consider ownership, lifetimes, `Send + Sync` bounds, extractor ordering, and the difference between `IntoResponse` (Axum) and `ResponseError` (Actix).

5. **Recommend** -- Provide concrete Rust code examples with explanations. Always qualify framework trade-offs.

6. **Verify** -- Suggest validation: `cargo build`, `cargo test`, `cargo clippy`, checking for `Send` bound issues in async handlers.

## Framework Selection Guide

| Dimension | Axum | Actix Web |
|---|---|---|
| Runtime model | Tokio (standard async) | Tokio + Actix actor runtime |
| Composition | Tower Service/Layer (functional) | App builder pattern (imperative) |
| State sharing | `Arc<AppState>` + `.with_state()` | `web::Data<T>` (internally Arc) |
| Middleware API | `from_fn`, Tower layers | `Transform` trait, `wrap_fn` |
| Error handling | `IntoResponse` for error types | `ResponseError` trait |
| WebSocket | `axum::extract::ws` | `actix-web-actors::ws` (actor-based) |
| Ecosystem fit | Tower, hyper, full Tokio ecosystem | Self-contained Actix ecosystem |
| Learning curve | Moderate (Tower concepts) | Steeper (actor model + Transform) |
| Performance | Excellent (~94% of Actix) | Historically highest TechEmpower |

### Choose Axum When

- Building new projects without specific actor-model needs
- Your team uses Tower middleware (tracing, auth, rate-limiting)
- You want maximum composability -- handlers, layers, services interoperate via the `Service` trait
- You need tight integration with hyper or other Tokio-ecosystem crates
- Building microservices that share middleware via a common `tower::Layer` library

### Choose Actix Web When

- You need actor-per-connection state machines for WebSocket at scale
- Raw throughput is critical and profiling confirms the difference matters
- Your team has existing Actix ecosystem investment (actix-session, actix-files)
- You need fine-grained per-worker isolation

**Default recommendation for new projects: Axum.** The Tower ecosystem backing and lower conceptual overhead make it the better starting point.

## Core Concepts

### Handlers (Axum)

Any async function whose arguments implement `FromRequestParts` (or `FromRequest`) and whose return type implements `IntoResponse` is a valid handler. No trait bounds in the function signature.

```rust
async fn get_user(
    State(state): State<Arc<AppState>>,
    Path(user_id): Path<u64>,
    Query(params): Query<Pagination>,
) -> Result<Json<User>, AppError> {
    let user = state.db.find_user(user_id).await?
        .ok_or(AppError::NotFound(format!("User {user_id}")))?;
    Ok(Json(user))
}
```

Extractors resolve left-to-right. Body-consuming extractors (`Json<T>`, `Bytes`) must be last.

### Handlers (Actix Web)

```rust
async fn get_user(
    path: web::Path<UserId>,
    query: web::Query<Pagination>,
    db: web::Data<DbPool>,
) -> Result<HttpResponse, AppError> {
    let user = db.find_user(path.id).await?
        .ok_or(AppError::NotFound("User".into()))?;
    Ok(HttpResponse::Ok().json(user))
}
```

### Extractors

| Extractor | Axum | Actix Web | Source |
|---|---|---|---|
| Path params | `Path<T>` | `web::Path<T>` | URL segment |
| Query string | `Query<T>` | `web::Query<T>` | Query params |
| JSON body | `Json<T>` | `web::Json<T>` | Request body |
| App state | `State<T>` | `web::Data<T>` | Shared state |
| Headers | `TypedHeader<H>` | `HttpRequest` | HTTP headers |
| Form data | `Form<T>` | `web::Form<T>` | URL-encoded body |

Both frameworks support custom extractors. In Axum, implement `FromRequestParts<S>` (non-body) or `FromRequest<S>` (body). In Actix, implement `FromRequest`.

## Error Handling

### Axum -- AppError + IntoResponse

```rust
use axum::{response::IntoResponse, http::StatusCode, Json};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum AppError {
    #[error("Not found: {0}")]
    NotFound(String),
    #[error("Validation: {0}")]
    Validation(String),
    #[error("Database error")]
    Database(#[from] sqlx::Error),
    #[error("Unauthorized")]
    Unauthorized,
    #[error("Internal error")]
    Internal(#[from] anyhow::Error),
}

impl IntoResponse for AppError {
    fn into_response(self) -> axum::response::Response {
        let (status, msg) = match &self {
            AppError::NotFound(m)   => (StatusCode::NOT_FOUND, m.clone()),
            AppError::Validation(m) => (StatusCode::UNPROCESSABLE_ENTITY, m.clone()),
            AppError::Database(_)   => (StatusCode::INTERNAL_SERVER_ERROR, "Database error".into()),
            AppError::Unauthorized  => (StatusCode::UNAUTHORIZED, "Unauthorized".into()),
            AppError::Internal(_)   => (StatusCode::INTERNAL_SERVER_ERROR, "Internal error".into()),
        };
        tracing::error!(error = ?self, "Request error");
        (status, Json(serde_json::json!({"error": msg}))).into_response()
    }
}
```

### Actix Web -- AppError + ResponseError

```rust
impl ResponseError for AppError {
    fn status_code(&self) -> actix_web::http::StatusCode { /* match variants */ }
    fn error_response(&self) -> HttpResponse {
        HttpResponse::build(self.status_code())
            .json(serde_json::json!({"error": self.to_string()}))
    }
}
```

### Error Crate Roles

- **thiserror** -- Define structured error enums with `#[derive(Error)]`. Use in libraries and domain layers.
- **anyhow** -- Catch-all `anyhow::Error` for application code. Add context with `.context("msg")`.
- **Neither replaces the other** -- Use `thiserror` for your `AppError` enum, `anyhow` for internal plumbing where you just need `?` propagation with context.

## State Management

```rust
// Axum: Arc<AppState> with .with_state()
#[derive(Clone)]
struct AppState {
    db: PgPool,
    config: Arc<Config>,
}

let state = Arc::new(AppState { db: pool, config: Arc::new(config) });
let app = Router::new()
    .route("/users/:id", get(get_user))
    .with_state(state);

// Actix: web::Data<T> (internally Arc)
let data = web::Data::new(AppState { db: pool, config });
App::new().app_data(data.clone())
```

For mutable shared state, use `tokio::sync::RwLock` or `DashMap` inside the state struct. Avoid `std::sync::Mutex` in async code -- it blocks the Tokio thread.

## Middleware

### Axum -- Tower Layers

```rust
use tower_http::{cors::CorsLayer, compression::CompressionLayer,
    timeout::TimeoutLayer, trace::TraceLayer};

let app = Router::new()
    .nest("/api", api_router)
    .layer(
        tower::ServiceBuilder::new()
            .layer(TraceLayer::new_for_http())
            .layer(TimeoutLayer::new(Duration::from_secs(30)))
            .layer(CompressionLayer::new())
            .layer(CorsLayer::permissive()),
    );

// Function middleware (simplest custom middleware)
async fn require_auth(request: Request, next: Next) -> Result<Response, StatusCode> {
    let token = request.headers().get("Authorization")
        .ok_or(StatusCode::UNAUTHORIZED)?;
    // validate...
    Ok(next.run(request).await)
}
app.route_layer(middleware::from_fn(require_auth));
```

### Actix -- wrap / wrap_fn

```rust
App::new()
    .wrap(middleware::Logger::default())
    .wrap(middleware::Compress::default())
    .wrap_fn(|req, srv| {
        println!("{} {}", req.method(), req.uri());
        srv.call(req)
    })
```

Full custom Actix middleware requires implementing the `Transform` trait, which is more verbose than Tower layers.

## Database Integration

| Library | Type | Key Feature | Best For |
|---|---|---|---|
| **SQLx** | Async query builder | Compile-time checked SQL (`query!` macro) | Most Rust web projects |
| **Diesel** | Sync ORM | Type-safe query DSL, schema from DB | Teams wanting ORM, sync code |
| **SeaORM** | Async ORM | Built on SQLx, ActiveRecord pattern | Full ORM with async support |

```rust
// SQLx compile-time checked query
let user = sqlx::query_as!(User,
    "SELECT id, name, email FROM users WHERE id = $1", id)
    .fetch_optional(&pool).await?;

// Diesel in async handler (requires spawn_blocking)
let users = tokio::task::spawn_blocking(move || {
    let conn = &mut pool.get().unwrap();
    users::table.filter(users::is_active.eq(true)).load::<User>(conn)
}).await??;
```

## Tokio Runtime

Both Axum and Actix run on Tokio. Key patterns:

- **`tokio::spawn`** -- Spawn async tasks onto the runtime
- **`spawn_blocking`** -- Run CPU-bound or sync code on a dedicated thread pool (required for Diesel)
- **`JoinSet`** -- Manage multiple concurrent tasks with structured results
- **`select!`** -- Race multiple futures, cancel losers
- **Channels** -- `mpsc`, `broadcast`, `watch` for inter-task communication

```rust
// Graceful shutdown
let (tx, rx) = tokio::sync::oneshot::channel::<()>();
tokio::spawn(async move { tokio::signal::ctrl_c().await.ok(); tx.send(()).ok(); });

let listener = TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app)
    .with_graceful_shutdown(async { rx.await.ok(); })
    .await?;
```

## Serde

All request/response types derive `Serialize`/`Deserialize`. Key attributes:

```rust
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UserResponse {
    pub user_id: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub avatar_url: Option<String>,
    #[serde(default)]
    pub is_active: bool,
}
```

## Testing

### Axum -- Tower oneshot

```rust
use tower::ServiceExt;
let response = app.oneshot(
    Request::builder().uri("/api/users/1").body(Body::empty()).unwrap()
).await.unwrap();
assert_eq!(response.status(), StatusCode::OK);
```

### Actix -- test utilities

```rust
let app = test::init_service(App::new().service(/* routes */)).await;
let req = test::TestRequest::get().uri("/api/users/1").to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
```

### Integration tests with reqwest

Spawn a real server on port 0, use `reqwest::Client` for full HTTP testing including headers, auth, and response parsing.

## Deployment

Rust compiles to a single static binary via musl. Final Docker image is typically 5-15 MB.

```dockerfile
FROM rust:1.82-slim AS builder
RUN rustup target add x86_64-unknown-linux-musl && apt-get update && apt-get install -y musl-tools
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs && \
    cargo build --release --target x86_64-unknown-linux-musl && \
    rm -f target/x86_64-unknown-linux-musl/release/deps/myapp*
COPY src ./src
RUN cargo build --release --target x86_64-unknown-linux-musl

FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/myapp /app
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 3000
ENTRYPOINT ["/app"]
```

Release profile for maximum optimization:

```toml
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
strip = true
```

## Common Pitfalls

1. **Body extractor not last in Axum handler** -- `Json<T>` consumes the body. Place it after `State`, `Path`, `Query`.
2. **Using `std::sync::Mutex` in async code** -- Blocks the Tokio worker thread. Use `tokio::sync::Mutex` or `RwLock`.
3. **Holding references across `.await` points** -- The compiler errors on non-`Send` types held across await. Clone or restructure.
4. **Diesel without `spawn_blocking`** -- Diesel is synchronous. Calling it directly in an async handler blocks the runtime.
5. **Missing `#[derive(Clone)]` on state** -- Axum's `State<T>` requires `T: Clone`. Wrap in `Arc` if cloning is expensive.
6. **Actix `Transform` complexity** -- For simple cases, use `wrap_fn` or Axum's `from_fn` instead of implementing the full trait.

## Reference Files

Load these for deep knowledge on specific topics:

- `references/architecture.md` -- Tower Service/Layer traits, Axum router and handler system, extractors (FromRequest/FromRequestParts), Actix App builder and actor model, Tokio runtime (multi-threaded scheduler, spawn_blocking, JoinSet), Serde derive macros, error handling patterns. **Load when:** architecture questions, extractor issues, middleware implementation, runtime behavior.
- `references/best-practices.md` -- Project structure, error handling (AppError implementing IntoResponse), testing (reqwest integration, mock services), database patterns (SQLx compile-time queries, connection pools), state management (Arc<AppState>), deployment (musl static binary, scratch Docker), performance (release profile, LTO), common crates. **Load when:** "how should I structure", testing strategy, deployment setup, performance optimization.

Files in this skill

  • SKILL.md14.4 KB
  • references/architecture.md22.8 KB
  • references/best-practices.md16.3 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…