Rust Learning Path — Chapter 23. Rust is increasingly used for high-performance backend services. Axum provides an ergonomic web framework built around Tokio and the Tower middleware ecosystem. This chapter builds a small REST API and explains the architecture you need for production services.
1. Create the project
cargo new rust_api cd rust_api cargo add axum tokio --features tokio/full cargo add serde --features derive cargo add serde_json tracing tracing-subscriber uuid2. Start a minimal server
use axum::{routing::get, Router}; #[tokio::main] async fn main() { let app = Router::new().route("/health", get(health)); let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap(); axum::serve(listener, app).await.unwrap(); } async fn health() -> &'static str { "ok" }3. Return JSON
use axum::Json; use serde::Serialize; #[derive(Serialize)] struct HealthResponse { status: &'static str } async fn health() -> Json<HealthResponse> { Json(HealthResponse { status: "ok" }) }4. Path parameters
use axum::extract::Path; async fn get_user(Path(id): Path<u64>) -> String { format!("user {id}") }5. Query parameters
use axum::extract::Query; use serde::Deserialize; #[derive(Deserialize)] struct ListParams { limit: Option<usize> } async fn list(Query(params): Query<ListParams>) -> String { format!("limit={:?}", params.limit) }6. Receive JSON
#[derive(serde::Deserialize)] struct CreateUser { name: String } async fn create(Json(input): Json<CreateUser>) -> Json<CreateUser> { Json(input) }7. Application state
use axum::extract::State; use std::sync::Arc; #[derive(Clone)] struct AppState { service_name: Arc<str> } async fn info(State(state): State<AppState>) -> String { state.service_name.to_string() }Put reusable clients and service handles in state, not request-specific data.
8. A structured application layout
src/ ├── main.rs ├── app.rs ├── config.rs ├── error.rs ├── routes/ │ ├── mod.rs │ ├── health.rs │ └── users.rs ├── services/ └── repositories/9. HTTP status codes
use axum::http::StatusCode; async fn create_user() -> (StatusCode, &'static str) { (StatusCode::CREATED, "created") }10. Central application errors
enum AppError { NotFound, Validation(String), Internal, }Implement IntoResponse so handlers can return Result. Keep internal database details out of public error responses.
11. Request validation
Validate length, format, bounds and required business rules at the API boundary. Never assume JSON deserialization means the data is safe or meaningful.
12. Middleware
Middleware is appropriate for cross-cutting concerns such as request IDs, tracing, CORS, authentication, timeouts and response headers. Keep business logic in services instead of middleware.
13. Request IDs
A request ID makes production debugging much easier. Accept a trusted incoming ID when appropriate or generate one, attach it to logs and return it in the response headers.
14. Tracing
tracing_subscriber::fmt() .with_target(false) .init();Add structured fields rather than constructing only free-form log strings.
15. Graceful shutdown
A production server should stop accepting new work, allow in-flight requests to finish within a deadline, close resources and then exit. Tokio signal handling can be connected to Axum's graceful shutdown support.
16. API versioning
let api = Router::new() .route("/users", get(list_users)); let app = Router::new().nest("/api/v1", api);17. Production checklist
- Configuration from environment/secrets, not hard-coded credentials.
- Structured errors and status codes.
- Input limits and validation.
- Request IDs and tracing.
- Timeouts around downstream calls.
- Health/readiness endpoints.
- Graceful shutdown.
- Tests for routes and service behavior.
Mini lab
Build a CRUD-style in-memory Notes API with GET /health, GET /api/v1/notes, POST /api/v1/notes and GET /api/v1/notes/{id}. Add application state, JSON errors and request logging.
Key takeaways
Axum keeps HTTP extraction and responses strongly typed. The bigger engineering lesson is separation: routes translate HTTP, services hold business logic, repositories access data, and middleware handles cross-cutting concerns.
Join the discussion
0 CommentsAsk questions, add context, or share what worked.
More from Melochip Editorial
Learn what Playwright testing is, why modern QA teams use it, how testers can get started, and the major types...
7 min read · Sep 19, 2026
Learn how Kubernetes Deployments and ReplicaSets work with practical YAML, scaling, self-healing, rolling upda...
7 min read · Sep 17, 2026
Learn Kubernetes security best practices with RBAC, Secrets, ServiceAccounts, Pod securityContext, NetworkPoli...
6 min read · Sep 17, 2026Related articles
Learn how Kubernetes Deployments and ReplicaSets work with practical YAML, scaling, self-healing, rolling upda...
7 min read · Sep 17, 2026
Learn Kubernetes Ingress with controllers, IngressClass, host and path routing, TLS, DNS, annotations, redirec...
8 min read · Sep 17, 2026
Prepare for Docker interviews with practical questions and real-time scenarios covering images, containers, Do...
2 min read · Sep 17, 2026
Learn what Playwright testing is, why modern QA teams use it, how testers can get started, and the major types...
7 min read · Sep 19, 2026Stay in the loop. Get new Melochip articles
Receive Melochip article updates when new articles are available. Explicit opt-in only.
No comments yet. Start the discussion.