| name | axum-service-architecture |
| description | Service architecture patterns for Axum applications including layered design (Router → Handler → Service → Repository), AppState with FromRef for dependency injection, Tower ServiceBuilder for middleware composition, and modular router organization. Use when designing service layers, managing dependencies, composing middleware stacks, or structuring Axum applications. |
Axum Service Architecture
Production service architecture patterns for layered Axum applications
Version Context
- Axum: 0.8.7
- Tower: 0.5.2
- Tower-HTTP: 0.6.x
When to Use This Skill
- Designing service layer architecture
- Managing application dependencies
- Composing middleware with Tower
- Organizing routers modularly
- Implementing dependency injection patterns
- Structuring production Axum applications
Service Layer Architecture
Layered Architecture Pattern
Client → Router → Tower Layers → Handler → Service → Repository → External
(Axum) (timeout/retry) (extract) (domain) (data) (I/O)
Benefits:
- Clear separation of concerns
- Easy to test each layer independently
- Maintainable and scalable structure
- Explicit dependencies
Architecture Example
use axum::{Router, routing::get};
use tower::ServiceBuilder;
use tower_http::{trace::TraceLayer, timeout::TimeoutLayer};
use std::time::Duration;
// Layer 1: Router (HTTP routing)
pub fn create_router(state: AppState) -> Router {
Router::new()
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(get_user))
.layer(
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(TimeoutLayer::new(Duration::from_secs(30)))
)
.with_state(state)
}
// Layer 2: Handler (HTTP concerns, extraction)
async fn create_user(
State(service): State<Arc<UserService>>,
Json(payload): Json<CreateUserRequest>,
) -> Result<Json<UserResponse>, ApiError> {
let user = service.create_user(payload).await?;
Ok(Json(user.into()))
}
// Layer 3: Service (business logic)
impl UserService {
pub async fn create_user(
&self,
request: CreateUserRequest,
) -> Result<User, UserError> {
// Validation
request.validate()?;
// Business logic
let user = User::new(request.email, request.name);
// Delegate to repository
self.repository.save_user(&user).await?;
Ok(user)
}
}
// Layer 4: Repository (data access)
#[async_trait]
pub trait UserRepository: Send + Sync {
async fn save_user(&self, user: &User) -> Result<(), RepositoryError>;
async fn find_user(&self, id: UserId) -> Result<User, RepositoryError>;
}
Dependency Management with AppState
AppState Pattern
use axum::extract::FromRef;
use std::sync::Arc;
/// Primary application state
#[derive(Clone, FromRef)]
pub struct AppState {
pub database: Arc<Database>,
pub cache: Arc<RedisClient>,
pub config: Arc<Config>,
pub user_service: Arc<UserService>,
pub order_service: Arc<OrderService>,
}
impl AppState {
/// Constructor with dependency wiring
pub async fn new(config: Config) -> Result<Self, AppError> {
let database = Arc::new(Database::connect(&config.database_url).await?);
let cache = Arc::new(RedisClient::connect(&config.redis_url).await?);
let user_service = Arc::new(UserService::new(
database.clone(),
cache.clone(),
));
let order_service = Arc::new(OrderService::new(
database.clone(),
user_service.clone(),
));
Ok(Self {
database,
cache,
config: Arc::new(config),
user_service,
order_service,
})
}
}
FromRef for Sub-Dependencies
// Extract specific dependencies from AppState
impl FromRef<AppState> for Arc<Database> {
fn from_ref(app_state: &AppState) -> Self {
app_state.database.clone()
}
}
impl FromRef<AppState> for Arc<UserService> {
fn from_ref(app_state: &AppState) -> Self {
app_state.user_service.clone()
}
}
// Handlers can extract exactly what they need
async fn handler(
State(db): State<Arc<Database>>, // Extracted via FromRef
State(service): State<Arc<UserService>>,
) -> Result<Json<Response>, ApiError> {
// Use only what's needed
let data = service.process(db).await?;
Ok(Json(data))
}
Tower ServiceBuilder Pattern
Middleware Composition
use tower::ServiceBuilder;
use tower_http::{
trace::TraceLayer,
timeout::TimeoutLayer,
compression::CompressionLayer,
cors::CorsLayer,
limit::{RequestBodyLimitLayer, ConcurrencyLimitLayer},
};
use std::time::Duration;
pub fn build_middleware_stack() -> ServiceBuilder<
tower::layer::util::Stack<
TraceLayer<SharedClassifier>,
tower::layer::util::Stack<TimeoutLayer, /* ... */>
>
> {
ServiceBuilder::new()
// Observability (first - captures all requests)
.layer(TraceLayer::new_for_http())
// Security
.layer(CorsLayer::permissive())
// Performance
.layer(CompressionLayer::new())
.layer(RequestBodyLimitLayer::new(1024 * 1024)) // 1MB
// Reliability
.layer(TimeoutLayer::new(Duration::from_secs(30)))
.layer(ConcurrencyLimitLayer::new(1000))
}
// Apply to router
let app = Router::new()
.route("/", get(handler))
.layer(build_middleware_stack())
.with_state(state);
Custom Middleware with State
use axum::middleware::{self, Next};
use axum::extract::{Request, State};
async fn auth_middleware(
State(auth_service): State<Arc<AuthService>>,
mut request: Request,
next: Next,
) -> Result<Response, ApiError> {
let token = request
.headers()
.get("authorization")
.ok_or(ApiError::MissingAuth)?;
let user = auth_service
.validate_token(token)
.await
.map_err(ApiError::InvalidAuth)?;
// Add authenticated user to extensions
request.extensions_mut().insert(user);
Ok(next.run(request).await)
}
// Apply with state
let app = Router::new()
.route("/protected", get(protected_handler))
.layer(middleware::from_fn_with_state(
state.clone(),
auth_middleware
))
.with_state(state);
Modular Router Organization
Router Composition
use axum::Router;
pub fn create_app(state: AppState) -> Router {
Router::new()
.nest("/api/v1", api_v1_routes())
.nest("/admin", admin_routes())
.merge(health_routes())
.with_state(state)
}
fn api_v1_routes() -> Router<AppState> {
Router::new()
.merge(user_routes())
.merge(order_routes())
.merge(product_routes())
}
fn user_routes() -> Router<AppState> {
Router::new()
.route("/users", get(list_users).post(create_user))
.route("/users/:id", get(get_user).put(update_user).delete(delete_user))
}
fn order_routes() -> Router<AppState> {
Router::new()
.route("/orders", get(list_orders).post(create_order))
.route("/orders/:id", get(get_order))
}
fn health_routes() -> Router<AppState> {
Router::new()
.route("/health", get(health_check))
.route("/ready", get(readiness_check))
.route("/metrics", get(metrics_handler))
}
fn admin_routes() -> Router<AppState> {
Router::new()
.route("/admin/users", get(admin_list_users))
.layer(middleware::from_fn(require_admin_role))
}
Configuration Management
Type-Safe Configuration
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct Config {
pub server: ServerConfig,
pub database: DatabaseConfig,
pub redis: RedisConfig,
pub observability: ObservabilityConfig,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
#[serde(with = "humantime_serde")]
pub request_timeout: Duration,
pub max_connections: usize,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DatabaseConfig {
pub url: String,
pub max_connections: u32,
#[serde(with = "humantime_serde")]
pub connection_timeout: Duration,
}
impl Config {
pub fn from_env() -> Result<Self, ConfigError> {
let config = config::Config::builder()
.add_source(config::File::with_name("config/default"))
.add_source(config::Environment::with_prefix("APP"))
.build()?;
let config: Self = config.try_deserialize()?;
config.validate()?;
Ok(config)
}
pub fn validate(&self) -> Result<(), ConfigError> {
if self.server.port == 0 {
return Err(ConfigError::InvalidPort);
}
if self.database.max_connections == 0 {
return Err(ConfigError::InvalidConnectionPool);
}
Ok(())
}
}
Service Container Pattern
Dependency Injection Container
use async_trait::async_trait;
pub struct ServiceContainer {
config: Arc<Config>,
database: Arc<dyn DatabaseConnection>,
cache: Arc<dyn CacheClient>,
metrics: Arc<dyn MetricsCollector>,
}
#[async_trait]
pub trait DatabaseConnection: Send + Sync {
async fn health_check(&self) -> Result<(), DatabaseError>;
async fn get_connection(&self) -> Result<Connection, DatabaseError>;
}
impl ServiceContainer {
pub async fn new(config: Config) -> Result<Self, ContainerError> {
let config = Arc::new(config);
let database = Arc::new(
PostgresDatabase::connect(&config.database).await?
);
let cache = Arc::new(
RedisCache::connect(&config.redis).await?
);
let metrics = Arc::new(PrometheusMetrics::new());
Ok(Self {
config,
database,
cache,
metrics,
})
}
pub fn user_service(&self) -> Arc<UserService> {
Arc::new(UserService::new(
self.database.clone(),
self.cache.clone(),
self.metrics.clone(),
))
}
pub fn into_app_state(self) -> AppState {
AppState {
database: self.database,
cache: self.cache,
config: self.config,
user_service: self.user_service(),
}
}
}
Best Practices
- Clear layer boundaries: Each layer has a single responsibility
- Dependency direction: Layers depend on abstractions, not implementations
- Explicit state: Use AppState and FromRef for dependency management
- Middleware ordering: Apply middleware in correct order (trace → auth → timeout)
- Modular routers: Organize routes by domain/module
- Configuration validation: Validate config at startup, fail fast
- Type-safe dependencies: Use Arc
for swappable implementations - Health checks: Verify all dependencies in health endpoints
Common Dependencies
[dependencies]
axum = { version = "0.8", features = ["macros"] }
tower = { version = "0.5", features = ["full"] }
tower-http = { version = "0.6", features = [
"trace", "timeout", "compression", "cors", "limit"
] }
config = "0.14"
serde = { version = "1", features = ["derive"] }
humantime-serde = "1"