Rust-skills rust-web
Rust web development expert covering HTTP frameworks (axum, actix), REST API design, handler patterns, state management, middleware, database integration, and domain-driven architecture.
install
source · Clone the upstream repo
git clone https://github.com/huiali/rust-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/huiali/rust-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/rust-web" ~/.claude/skills/huiali-rust-skills-rust-web-72945b && rm -rf "$T"
manifest:
skills/rust-web/SKILL.mdsource content
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 | + serde |
| Configuration | crate + environment variables |
| Logging | + |
| Health check | endpoint returning 200 |
| CORS | |
| Rate limiting | |
| OpenAPI | for API documentation |
| Request validation | crate with #[validate] |
| Graceful shutdown | for SIGTERM handling |
Common Pitfalls
| Pitfall | Problem | Solution |
|---|---|---|
Using for state | Not thread-safe | Use |
Holding locks across | Potential deadlock | Minimize lock scope |
| Not handling errors | Handler panics | Implement for errors |
| Large request bodies | Memory pressure | Set body size limits with |
| Missing CORS headers | Browser blocks requests | Add |
| Synchronous blocking | Blocks executor | Use 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
, domain-specific logic stays localshared/
File Responsibilities:
- HTTP routes, handlers, request/response DTOshttp.rs
- Application services, use case orchestrationapp.rs
- Domain entities with business logicentity.rs
- Port trait definitions (hexagonal architecture)port.rs
- Repository implementations (database, cache)repo.rs
- Domain-specific error typeserrors.rs
Review Checklist
When reviewing web service code:
- Handlers have appropriate extractors (Path, Query, Json)
- Shared state uses
for thread safetyArc - 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 - 完整中文版本,包含所有内容