Agent Skills: Check compilation

Rust web development expert covering HTTP frameworks (axum, actix), REST API design, handler patterns, state management, middleware, database integration, and domain-driven architecture.

UncategorizedID: huiali/rust-skills/rust-web

Install this agent skill to your local

pnpm dlx add-skill https://github.com/huiali/rust-skills/tree/HEAD/.codex/skills/rust-web

Skill Files

Browse the full folder contents for rust-web.

Download Skill

Loading file tree…

.codex/skills/rust-web/SKILL.md

Skill Metadata

Name
rust-web
Description
Rust web development expert covering HTTP frameworks (axum, actix), REST API design, handler patterns, state management, middleware, database integration, and domain-driven architecture.

Framework Selection

| Framework | Characteristics | Recommended For | |-----------|-----------------|-----------------| | axum | Modern, Tokio ecosystem, type-safe | New projects (default choice) | | actix-web | High performance, Actor model | Performance-critical services | | rocket | Developer-friendly, zero-config | Rapid prototyping | | warp | Filter-based, functional style | Niche use cases |

Recommendation: Start with axum for most projects. It has excellent ergonomics, strong ecosystem integration, and active development.

Solution Patterns

Pattern 1: Axum Basic Structure

use axum::{routing::get, Router};

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/", get(root))
        .route("/users", get(list_users).post(create_user))
        .route("/users/:id", get(get_user).delete(delete_user))
        .with_state(app_state());

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();

    axum::serve(listener, app)
        .await
        .unwrap();
}

Key insight: Routes are type-safe, handlers are async functions.

Pattern 2: Handler Patterns

use axum::{extract::{Path, Query, Json}, http::StatusCode};

// Path parameters
async fn get_user(Path(id): Path<u32>) -> Result<Json<User>, StatusCode> {
    User::find(id).await
        .map(Json)
        .ok_or(StatusCode::NOT_FOUND)
}

// JSON body
async fn create_user(
    Json(payload): Json<CreateUserRequest>
) -> Result<Json<User>, StatusCode> {
    User::create(payload).await
        .map(Json)
        .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
}

// Query parameters
async fn list_users(
    Query(params): Query<ListUsersParams>
) -> Json<Vec<User>> {
    Json(User::list(params).await)
}

// Multiple extractors
async fn update_user(
    Path(id): Path<u32>,
    State(db): State<DbPool>,
    Json(update): Json<UserUpdate>,
) -> Result<Json<User>, ApiError> {
    User::update(&db, id, update).await
        .map(Json)
}

When to use: Each extractor pattern for different input types.

Pattern 3: State Management

use std::sync::Arc;
use sqlx::PgPool;

// Define shared state
#[derive(Clone)]
struct AppState {
    db: PgPool,
    config: Arc<Config>,
}

// Extract state in handlers
async fn handler(State(state): State<AppState>) -> Json<Response> {
    let user = User::fetch(&state.db, 123).await?;
    Json(Response { user })
}

// Setup
let state = AppState {
    db: PgPoolOptions::new()
        .max_connections(5)
        .connect(&db_url)
        .await?,
    config: Arc::new(load_config()),
};

let app = Router::new()
    .route("/", get(handler))
    .with_state(state);

When to use: Share database pools, configuration, clients across handlers.

Trade-offs: State must be Clone + Send + Sync + 'static.

Pattern 4: Error Handling

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

#[derive(Error, Debug)]
pub enum ApiError {
    #[error("resource not found")]
    NotFound,

    #[error("invalid input: {0}")]
    Validation(String),

    #[error("database error")]
    Database(#[from] sqlx::Error),

    #[error("authentication required")]
    Unauthorized,
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            ApiError::NotFound => (StatusCode::NOT_FOUND, self.to_string()),
            ApiError::Validation(msg) => (StatusCode::BAD_REQUEST, msg),
            ApiError::Database(e) => {
                tracing::error!("database error: {}", e);
                (StatusCode::INTERNAL_SERVER_ERROR, "internal error".to_string())
            }
            ApiError::Unauthorized => (StatusCode::UNAUTHORIZED, self.to_string()),
        };

        (status, Json(serde_json::json!({
            "error": message
        }))).into_response()
    }
}

When to use: Custom error types for domain-specific failures.

Workflow

Step 1: Choose Framework

Need high performance?
  → actix-web

Want modern ergonomics + Tokio ecosystem?
  → axum (recommended)

Rapid prototyping?
  → rocket

Step 2: Design Handler Signatures

What data comes from?
  Path → Path<T>
  Query string → Query<T>
  JSON body → Json<T>
  Headers → TypedHeader<T>
  State → State<AppState>

Step 3: Implement Error Handling

Library code?
  → Custom error enum + IntoResponse

Application code?
  → anyhow::Error with context

Step 4: Add Middleware

Logging → tower_http::trace
CORS → tower_http::cors
Rate limiting → tower::limit
Authentication → custom middleware

Middleware Patterns

Logging Middleware

use axum::{
    middleware::{self, Next},
    http::Request,
    response::Response,
};
use std::time::Instant;

async fn log_requests<B>(
    req: Request<B>,
    next: Next<B>,
) -> Response {
    let start = Instant::now();
    let method = req.method().clone();
    let uri = req.uri().clone();

    let response = next.run(req).await;

    tracing::info!(
        "{} {} {} - {:?}",
        method,
        uri,
        response.status(),
        start.elapsed()
    );

    response
}

// Apply middleware
let app = Router::new()
    .route("/", get(handler))
    .layer(middleware::from_fn(log_requests));

Authentication Middleware

use axum::{
    middleware,
    extract::Request,
    http::{StatusCode, header},
};

async fn auth_middleware(
    mut req: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    let auth_header = req.headers()
        .get(header::AUTHORIZATION)
        .and_then(|h| h.to_str().ok())
        .ok_or(StatusCode::UNAUTHORIZED)?;

    let user = validate_token(auth_header)
        .ok_or(StatusCode::UNAUTHORIZED)?;

    req.extensions_mut().insert(user);
    Ok(next.run(req).await)
}

Database Integration

SQLx Pattern

use sqlx::{PgPool, FromRow};
use chrono::{DateTime, Utc};

// Define model
#[derive(Debug, FromRow)]
struct User {
    id: i32,
    name: String,
    email: String,
    created_at: DateTime<Utc>,
}

// Query
async fn get_user(pool: &PgPool, id: i32) -> Result<User, sqlx::Error> {
    sqlx::query_as!(
        User,
        "SELECT id, name, email, created_at FROM users WHERE id = $1",
        id
    )
    .fetch_one(pool)
    .await
}

// Transaction
async fn create_user_with_profile(
    pool: &PgPool,
    user: NewUser,
) -> Result<User, sqlx::Error> {
    let mut tx = pool.begin().await?;

    let user_id = sqlx::query!(
        "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
        user.name,
        user.email
    )
    .fetch_one(&mut *tx)
    .await?
    .id;

    sqlx::query!(
        "INSERT INTO profiles (user_id, bio) VALUES ($1, $2)",
        user_id,
        user.bio
    )
    .execute(&mut *tx)
    .await?;

    tx.commit().await?;

    get_user(pool, user_id).await
}

Best Practices

| Concern | Recommendation | |---------|----------------| | JSON serialization | #[derive(Serialize, Deserialize)] + serde | | Configuration | config crate + environment variables | | Logging | tracing + tracing-subscriber | | Health check | GET /health endpoint returning 200 | | CORS | tower_http::cors::CorsLayer | | Rate limiting | tower::limit::RateLimitLayer | | OpenAPI | utoipa for API documentation | | Request validation | validator crate with #[validate] | | Graceful shutdown | tokio::signal for SIGTERM handling |

Common Pitfalls

| Pitfall | Problem | Solution | |---------|---------|----------| | Using Rc for state | Not thread-safe | Use Arc | | Holding locks across .await | Potential deadlock | Minimize lock scope | | Not handling errors | Handler panics | Implement IntoResponse for errors | | Large request bodies | Memory pressure | Set body size limits with DefaultBodyLimit | | Missing CORS headers | Browser blocks requests | Add CorsLayer | | Synchronous blocking | Blocks executor | Use spawn_blocking for CPU work |

Domain-Driven Project Structure

Recommended structure for medium-to-large projects:

src/
├── main.rs                              # Entry point (load config, start HTTP)
├── bootstrap/
│   ├── mod.rs
│   └── app_builder.rs                   # Global assembly (DB/Cache/Telemetry)
├── domains/
│   ├── user/                            # Domain directory with all layers
│   │   ├── mod.rs
│   │   ├── http.rs                      # Routes + handlers + DTO mapping
│   │   ├── app.rs                       # Use cases / commands / queries
│   │   ├── entity.rs                    # Domain entities
│   │   ├── value.rs                     # Value objects
│   │   ├── policy.rs                    # Domain rules/policies
│   │   ├── port.rs                      # Port definitions (traits)
│   │   ├── repo.rs                      # Infrastructure implementation
│   │   ├── cache.rs                     # Caching adapter
│   │   ├── errors.rs                    # Domain errors
│   │   └── tests.rs                     # Domain tests
│   ├── auth/
│   │   ├── mod.rs
│   │   ├── http.rs
│   │   ├── app.rs
│   │   ├── entity.rs
│   │   ├── policy.rs
│   │   ├── port.rs
│   │   ├── repo.rs
│   │   ├── jwt.rs
│   │   ├── errors.rs
│   │   └── tests.rs
│   └── order/
│       ├── mod.rs
│       ├── http.rs
│       ├── app.rs
│       ├── entity.rs
│       ├── port.rs
│       ├── repo.rs
│       ├── events.rs
│       ├── errors.rs
│       └── tests.rs
├── shared/
│   ├── mod.rs
│   ├── error.rs                         # Common error model
│   ├── result.rs                        # Unified Result alias
│   ├── types.rs                         # Common types (ID/Time)
│   └── middleware.rs                    # Cross-domain middleware
└── tests/
    ├── integration/
    └── fixtures/

Structural Principles:

  • Domain-centric: Each domain contains interface/application/domain/infrastructure concerns
  • File naming: Single-word filenames clarify responsibility (app.rs, port.rs, repo.rs, http.rs)
  • Loose coupling: Domains collaborate through application-layer interfaces, avoid direct access
  • Shared utilities: Common capabilities in shared/, domain-specific logic stays local

File Responsibilities:

  • http.rs - HTTP routes, handlers, request/response DTOs
  • app.rs - Application services, use case orchestration
  • entity.rs - Domain entities with business logic
  • port.rs - Port trait definitions (hexagonal architecture)
  • repo.rs - Repository implementations (database, cache)
  • errors.rs - Domain-specific error types

Review Checklist

When reviewing web service code:

  • [ ] Handlers have appropriate extractors (Path, Query, Json)
  • [ ] Shared state uses Arc for thread safety
  • [ ] Error types implement IntoResponse
  • [ ] Database operations use connection pooling
  • [ ] Middleware is composable and reusable
  • [ ] API responses follow consistent JSON format
  • [ ] Authentication/authorization properly enforced
  • [ ] Request body size limits configured
  • [ ] CORS configured for browser clients
  • [ ] Health check endpoint exists
  • [ ] Logging/tracing properly instrumented
  • [ ] Graceful shutdown implemented

Verification Commands

# Check compilation
cargo check

# Run tests
cargo test

# Integration tests
cargo test --test integration

# Check for common mistakes
cargo clippy -- -D warnings

# Run development server
cargo run

# Build optimized release
cargo build --release

# Run with environment variables
DATABASE_URL=postgres://localhost cargo run

Performance Optimization

Connection Pooling

use sqlx::postgres::PgPoolOptions;

let pool = PgPoolOptions::new()
    .max_connections(100)
    .min_connections(10)
    .acquire_timeout(Duration::from_secs(5))
    .idle_timeout(Duration::from_secs(600))
    .connect(&database_url)
    .await?;

Response Compression

use tower_http::compression::CompressionLayer;

let app = Router::new()
    .route("/", get(handler))
    .layer(CompressionLayer::new());

Caching Headers

use axum::http::{header, HeaderMap};

async fn cached_handler() -> (HeaderMap, Json<Data>) {
    let mut headers = HeaderMap::new();
    headers.insert(
        header::CACHE_CONTROL,
        "public, max-age=3600".parse().unwrap(),
    );

    (headers, Json(get_data()))
}

Related Skills

  • rust-async - Async patterns for handlers
  • rust-concurrency - Thread safety in web services
  • rust-database - Database integration patterns
  • rust-error - Error handling strategies
  • rust-auth - Authentication and authorization
  • rust-middleware - Middleware patterns
  • rust-observability - Logging and metrics

Localized Reference

  • Chinese version: SKILL_ZH.md - 完整中文版本,包含所有内容