# Rust Web

> When to activate: Rust web development, axum, actix-web, HTTP server, REST API, middleware, routing, handlers

- Skill: `mattakushi432/rust-web` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/rust-web`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/rust-web/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/rust-web

---


# Rust Web Development Patterns

## Axum: Minimal REST API

```toml
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
```

```rust
use axum::{
    extract::{Path, Query, State},
    http::StatusCode,
    response::Json,
    routing::{get, post},
    Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;

#[derive(Clone)]
struct AppState { db: Arc<Database> }

#[derive(Serialize, Deserialize)]
struct User { id: u64, name: String, email: String }

#[derive(Deserialize)]
struct CreateUser { name: String, email: String }

#[derive(Deserialize)]
struct Pagination { page: Option<u32>, limit: Option<u32> }

async fn list_users(
    Query(p): Query<Pagination>,
    State(state): State<AppState>,
) -> Result<Json<Vec<User>>, StatusCode> {
    state.db.list_users(p.page.unwrap_or(1), p.limit.unwrap_or(20))
        .await
        .map(Json)
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
}

async fn get_user(
    Path(id): Path<u64>,
    State(state): State<AppState>,
) -> Result<Json<User>, StatusCode> {
    state.db.find_user(id).await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
        .map(Json)
        .ok_or(StatusCode::NOT_FOUND)
}

async fn create_user(
    State(state): State<AppState>,
    Json(payload): Json<CreateUser>,
) -> Result<(StatusCode, Json<User>), StatusCode> {
    let user = state.db.create_user(payload.name, payload.email)
        .await
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    Ok((StatusCode::CREATED, Json(user)))
}

#[tokio::main]
async fn main() {
    let state = AppState { db: Arc::new(Database::new().await) };
    let app = Router::new()
        .route("/users", get(list_users).post(create_user))
        .route("/users/:id", get(get_user))
        .with_state(state);
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
```

## Axum: Middleware and Layers

```rust
use axum::middleware::{self, Next};
use axum::http::{Request, Response};
use tower::ServiceBuilder;
use tower_http::{cors::CorsLayer, trace::TraceLayer, timeout::TimeoutLayer, compression::CompressionLayer};
use std::time::Duration;

async fn auth_middleware<B>(
    State(state): State<AppState>,
    mut req: Request<B>,
    next: Next<B>,
) -> Result<Response, StatusCode> {
    let token = req.headers()
        .get("Authorization")
        .and_then(|v| v.to_str().ok())
        .and_then(|v| v.strip_prefix("Bearer "))
        .ok_or(StatusCode::UNAUTHORIZED)?;

    let claims = state.auth.verify(token).map_err(|_| StatusCode::UNAUTHORIZED)?;
    req.extensions_mut().insert(claims);
    Ok(next.run(req).await)
}

fn build_app(state: AppState) -> Router {
    let protected = Router::new()
        .route("/profile", get(get_profile))
        .layer(middleware::from_fn_with_state(state.clone(), auth_middleware));

    Router::new()
        .route("/health", get(health_check))
        .merge(protected)
        .layer(ServiceBuilder::new()
            .layer(TraceLayer::new_for_http())
            .layer(TimeoutLayer::new(Duration::from_secs(30)))
            .layer(CompressionLayer::new())
            .layer(CorsLayer::permissive()))
        .with_state(state)
}
```

## Typed Error Responses

```rust
use axum::{http::StatusCode, response::{IntoResponse, Response}, Json};
use serde_json::json;

enum ApiError {
    NotFound(String),
    Unauthorized,
    BadRequest(String),
    Internal(anyhow::Error),
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (status, message) = match &self {
            ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
            ApiError::Unauthorized => (StatusCode::UNAUTHORIZED, "unauthorized".into()),
            ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
            ApiError::Internal(e) => {
                tracing::error!("internal error: {e:#}");
                (StatusCode::INTERNAL_SERVER_ERROR, "internal server error".into())
            }
        };
        (status, Json(json!({ "error": message }))).into_response()
    }
}
```

## Actix-web Alternative

```toml
[dependencies]
actix-web = "4"
```

```rust
use actix_web::{web, App, HttpServer, HttpResponse, middleware};

async fn get_user(path: web::Path<u64>, data: web::Data<AppState>) -> HttpResponse {
    match data.db.find_user(*path).await {
        Ok(Some(user)) => HttpResponse::Ok().json(user),
        Ok(None) => HttpResponse::NotFound().finish(),
        Err(_) => HttpResponse::InternalServerError().finish(),
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let state = web::Data::new(AppState::new().await);
    HttpServer::new(move || {
        App::new()
            .app_data(state.clone())
            .wrap(middleware::Logger::default())
            .route("/users/{id}", web::get().to(get_user))
    })
    .bind("0.0.0.0:8080")?
    .run()
    .await
}
```

## Common Anti-Patterns

- **Cloning large state in every handler** — wrap expensive resources in `Arc<T>` inside `AppState`
- **Blocking in async handlers** — use `tokio::task::spawn_blocking` for CPU-bound work
- **Returning plain `StatusCode` without a body** — always include an error message
- **No request timeout** — always set a global timeout via `TimeoutLayer`
- **Storing secrets as plain strings** — use `secrecy::Secret<T>` or a secrets manager

