Rust Web Development Patterns
Axum: Minimal REST API
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
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
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
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
[dependencies]
actix-web = "4"
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