Axum is routing + extractors on top of Tower/Hyper/Tokio. Small handlers, explicit state, composition via Router and Tower middleware.
Typical stack
| Crate | Role |
|---|---|
axum |
Router, extractors, response |
tokio |
Async runtime |
tower / tower-http |
Middleware (trace, cors, compression, timeout) |
serde |
JSON / form |
tracing |
Structured logs |
| DB | sqlx or ORM already in the project |
Preserve the existing stack. Do not impose new ORMs without being asked. Runtime: tokio. Logs: tracing. HTTP client: reqwest. ORM: sea-orm / diesel if the repo already uses it; otherwise sqlx.
Organization
src/
main.rs # boot, config, serve
lib.rs # app() testável
routes/ # Router por domínio
handlers/
extractors/ # AuthUser, Pagination, etc.
error.rs # AppError + IntoResponse
state.rs # AppState
services/
app()returnsRouterfor tests without socket bind.- Routes by domain;
mainonly configures and serves. - State in
State<AppState>(Arcinternal when needed).
Non-negotiable rules
- Handlers
asyncreturningimpl IntoResponseorResult<T, AppError>. - Input via typed extractors (
Json,Query,Path,State, custom). - One
AppErrorcentral with HTTP status and stable body. - Authn/authz on the server (extractor/middleware), never only on the client.
- Conscious timeouts and body limits.
- Do not block the Tokio runtime in handlers.
Routing
Router::new()
.nest("/api/v1", api_router)
.route("/health", get(health))
.layer(TraceLayer::new_for_http())
.with_state(state)
- Group by prefix (
nest/merge). - Auth middleware in
Routernested, not on every public route. - Explicit method routing:
get,post,put,patch,delete.
Extractors
Order matters: extractors consume parts of the request.
Path<T>,Query<T>,Json<T>,Form<T>State<S>,Extension<T>(preferStatefor app state)- Custom extractor for
AuthUser/CurrentUser - Extractor rejections → 4xx response consistent with
AppError
Validate after decode (size, ranges, enums). Serde does not replace business rules.
State and dependencies
#[derive(Clone)]
struct AppState {
db: PgPool,
// configs baratas / Arc<Config>
}
- Cheap clone (handles,
Arc). - No mutable globals; no
lazy_staticfor DB. - Services receive what they need (pool, clients), not the whole
Request.
Errors
enum AppError {
NotFound,
Unauthorized,
Forbidden,
Validation(String),
Internal(anyhow::Error),
}
impl IntoResponse for AppError { /* status + JSON { code, message } */ }
- 4xx for client; 5xx for internal failures.
- Log
Internalwith tracing; generic external message. - Do not leak
Debugfrom anyhow to the client in production.
Tower Middleware
Use tower-http for the common cases:
TraceLayer,CorsLayer,CompressionLayer,TimeoutLayerRequestBodyLimitLayer/ default body limitsSetSensitiveHeadersLayerfor Authorization
Custom middleware: from_fn / Tower services when you need auth or correlation id.
Tests
tower::ServiceExt::oneshotor project helpers overRouter.- Isolated test DB or mocks at the infrastructure edge.
- Cover 200, 401, 403, 404, validation.
Anti-patterns
- Monolithic business logic in
main.rs unwrapin handlers- Sharing
Requestparts between extractors in a fragile way - CORS
*with cookies/credentials - Ignoring cancellation/timeouts in internal HTTP clients
- Mixing Actix/Rocket patterns unnecessarily
Integration
- Language and crates:
rust - Runtime:
tokio - Languages:
rust-best-practices - Production:
rust-axum-production - Logs:
tracing - Client:
reqwest - WS:
rust-axum-websocket - Contracts:
api-design - i18n:
i18n-backend
Completion criteria
Routercomposable and testable viaapp()- Consistent extractors and
AppErrorconsistent - Authz on sensitive routes
- Trace/timeout/limits middleware when applicable
- Tests of critical flows passing