HTTP Server with Axum
What production-grade means at this level
For a first real Rust web service, production-grade does not mean a huge framework setup. It means you can deploy, observe, and troubleshoot the service without guessing. The baseline should include: predictable startup, clear health signals, structured logs, and at least one safety rail (timeout).
Minimal service goals
- Small routing surface: start with one or two endpoints plus health checks.
- Health semantics: separate liveness from readiness so rollouts are safe later.
- Observable by default: request logs with latency and status code.
- Safe defaults: timeout to avoid stuck requests consuming capacity.
Dependencies
Keep dependencies minimal and explicit. Axum sits on Tokio. For baseline observability, add tracing and tower-http.
# Cargo.toml (core pieces)
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tower = "0.4"
tower-http = { version = "0.5", features = ["trace", "timeout"] }
Service skeleton
This is a clean single-file skeleton. In a real codebase you would split routes and config into modules, but keeping it in one place is perfect for the first step.
use axum::{routing::get, Router};
use std::{net::SocketAddr, time::Duration};
use tower::ServiceBuilder;
use tower_http::{timeout::TimeoutLayer, trace::TraceLayer};
use tracing_subscriber::EnvFilter;
async fn healthz() -> "static str {
"ok"
}
async fn readyz() -> "static str {
"ok"
}
fn build_router() -> Router {
Router::new()
.route("/healthz", get(healthz))
.route("/readyz", get(readyz))
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_target(false)
.with_level(true)
.init();
let app = build_router().layer(
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(TimeoutLayer::new(Duration::from_secs(10))),
);
let addr: SocketAddr = "0.0.0.0:3000".parse().unwrap();
tracing::info!(%addr, "server starting");
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
Health endpoints: liveness vs readiness
Use two endpoints even if they return the same response at first. This is a production habit that pays off later:
- /healthz (liveness): answers the question: should the process be restarted? Keep it cheap and independent of external dependencies.
- /readyz (readiness): answers the question: should the load balancer send traffic? Later, you can check database connectivity, migrations state, or required downstreams here.
At this stage both return ok, but the contract is already correct.
Structured logging: what you want in incidents
In incidents, you typically want: path, method, status, latency, and a correlation id. TraceLayer gives a baseline request log. In later lessons we will add request id propagation and structured fields across the request lifecycle.
Timeouts: a safety rail, not tuning
A timeout prevents a slow downstream or handler bug from pinning worker threads indefinitely. This is not micro-optimization. It is a simple guard that reduces blast radius. Start with a conservative value (for example 10 seconds) and adjust based on real latency data.
Environment-driven behavior
The logger is configured to read an environment filter. Example:
RUST_LOG=info ./your-binary RUST_LOG=tower_http=debug,info ./your-binary
This is production friendly because you can increase verbosity during incident response without changing code.
Common operational checks
- Startup logs: confirm the bind address and that the process is alive.
- Health probes: curl both endpoints from inside the container or VM.
- Timeout behavior: intentionally add a slow handler locally and confirm the timeout returns cleanly.
curl -i http://localhost:3000/healthz curl -i http://localhost:3000/readyz
What comes next
After the server boots reliably, you want control over routing and middleware composition, and then consistent JSON error responses. Those are the building blocks for stable APIs and predictable incident debugging.