skills/backend/rust-web/SKILL.md
Expert agent 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".
npx skillsauth add chrishuffman5/domain-expert backend-rust-webInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are a specialist in Rust web development covering 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>.
Classify the request:
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 patternsreferences/best-practices.md for project structure, error handling (AppError + IntoResponse), testing, database patterns, state management, deployment, performance tuning, common cratesSend + Sync bounds, and extractor orderingIdentify framework -- Determine from imports (axum::, actix_web::, tower::, actix::) or explicit mention. Default to Axum for new projects unless actor-model requirements exist.
Load context -- Read the relevant reference file before answering.
Analyze -- Apply Rust-specific reasoning. Consider ownership, lifetimes, Send + Sync bounds, extractor ordering, and the difference between IntoResponse (Axum) and ResponseError (Actix).
Recommend -- Provide concrete Rust code examples with explanations. Always qualify framework trade-offs.
Verify -- Suggest validation: cargo build, cargo test, cargo clippy, checking for Send bound issues in async handlers.
| 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 |
Service traittower::Layer libraryDefault recommendation for new projects: Axum. The Tower ecosystem backing and lower conceptual overhead make it the better starting point.
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.
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.
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))
}
| 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.
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()
}
}
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()}))
}
}
#[derive(Error)]. Use in libraries and domain layers.anyhow::Error for application code. Add context with .context("msg").thiserror for your AppError enum, anyhow for internal plumbing where you just need ? propagation with context.// 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.
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));
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.
| 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 |
// 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??;
Both Axum and Actix run on Tokio. Key patterns:
tokio::spawn -- Spawn async tasks onto the runtimespawn_blocking -- Run CPU-bound or sync code on a dedicated thread pool (required for Diesel)JoinSet -- Manage multiple concurrent tasks with structured resultsselect! -- Race multiple futures, cancel losersmpsc, broadcast, watch for inter-task communication// 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?;
All request/response types derive Serialize/Deserialize. Key attributes:
#[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,
}
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);
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());
Spawn a real server on port 0, use reqwest::Client for full HTTP testing including headers, auth, and response parsing.
Rust compiles to a single static binary via musl. Final Docker image is typically 5-15 MB.
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:
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
strip = true
Json<T> consumes the body. Place it after State, Path, Query.std::sync::Mutex in async code -- Blocks the Tokio worker thread. Use tokio::sync::Mutex or RwLock..await points -- The compiler errors on non-Send types held across await. Clone or restructure.spawn_blocking -- Diesel is synchronous. Calling it directly in an async handler blocks the runtime.#[derive(Clone)] on state -- Axum's State<T> requires T: Clone. Wrap in Arc if cloning is expensive.Transform complexity -- For simple cases, use wrap_fn or Axum's from_fn instead of implementing the full trait.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.tools
kubectl command-line usage and scripting: kubeconfig and context management, output formats (jsonpath, custom-columns, go-template), all major verbs (get, describe, apply, delete, exec, logs, port-forward, rollout, scale, drain), workload resources, config/storage, networking, RBAC, node management, debugging (CrashLoopBackOff, ImagePullBackOff, OOMKilled), and scripting patterns (dry-run, diff, wait, jq, kustomize). WHEN: "kubectl", "k8s CLI", "kubeconfig", "namespace", "pod", "deployment", "service", "ingress", "configmap", "secret", "rollout", "scale", "drain", "taint", "kustomize". Do NOT use for cluster architecture, sizing, upgrades, or workload design decisions — that's the `kubernetes` skill in the `containers` plugin. This skill is command syntax and scripting kubectl against an existing cluster, not cluster ops.
tools
Bash 5.x shell scripting, Unix text processing, and command-line automation: variables, parameter expansion, quoting, control flow, functions, I/O redirection, error handling (set -euo pipefail, trap), and the Unix tool ecosystem (grep, sed, awk, jq, find, sort, uniq, cut, xargs). Covers process management, networking (curl, ssh, rsync, nc), file locking (flock), parallel execution, and production script patterns. WHEN: "Bash", "bash", "shell", "sh", ".sh", "shell script", "sed", "awk", "grep", "jq", "find", "xargs", "curl", "ssh", "rsync", "cron", "pipe", "redirect", "here-doc", "shebang", "POSIX", "set -euo pipefail", "trap".
tools
Azure CLI (az) command syntax and scripting: authentication (interactive, service principal, managed identity, SSO), output formats and JMESPath queries, resource groups, VMs, storage accounts/blobs, networking (VNets, NSGs, load balancers, DNS), Entra ID, AKS, App Service, Functions, databases (SQL, Cosmos DB, MySQL, PostgreSQL), Key Vault, Monitor/alerting, and infrastructure scripting patterns. WHEN: "az ", "Azure CLI", "az login", "az vm", "az aks", "az storage", "az keyvault", "az monitor", "az ad", "az group", "az network", "az webapp", "az functionapp", "az sql", "az cosmosdb", "JMESPath", "az account". Do NOT use for Azure architecture, landing zones, or multi-subscription strategy — that's the `cloud-platforms` plugin. This skill is about command syntax and scripting the CLI, not deciding what to provision.
tools
AWS CLI v2 command syntax and scripting: authentication (profiles, SSO, assume-role, instance profiles), output formats and JMESPath queries, pagination and waiters, IAM, S3, Lambda, RDS, CloudFormation, ECS, EKS, CloudWatch, SSM, Route 53, STS, and VPC networking. WHEN: "aws ", "AWS CLI", "aws ec2", "aws s3", "aws lambda", "aws iam", "aws cloudformation", "aws ssm", "aws ecs", "aws eks", "aws rds", "aws cloudwatch", "aws route53", "aws sts". Do NOT use for AWS architecture, service selection, multi-account strategy, or FinOps — that's the `cloud-platforms` plugin. This skill is about command syntax and scripting the CLI, not deciding what to provision.