Axum provides routing and request handling on top of the Tower service model. This tutorial builds a deliberately small JSON API, then shows where validation, state, and middleware belong. It is a learning example, not a production deployment recipe.
Learning outcomes
- create an Axum router and serve it with Tokio;
- parse a path parameter and JSON request body;
- share application state explicitly;
- separate validation errors from transport failures;
- identify the additional controls required before production.
Prerequisites
Have stable Rust and Cargo installed. Create a project with cargo new and add
versions compatible with the current stable Axum release after checking the
official release documentation.
[dependencies]
axum = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] }
The version ranges are illustrative and must be resolved and locked during implementation. Do not copy them into a production project without review.
Start with a route
use axum::{routing::get, Router};
async fn health() -> &'static str {
"ok"
}
#[tokio::main]
async fn main() {
let app = Router::new().route("/health", get(health));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.expect("bind listener");
axum::serve(listener, app).await.expect("serve application");
}
Binding to loopback keeps this example local. A deployed service needs an explicit listener, proxy, TLS, shutdown, and observability policy.
Return JSON from a handler
use axum::Json;
use serde::Serialize;
#[derive(Serialize)]
struct Greeting {
message: &'static str,
}
async fn greeting() -> Json<Greeting> {
Json(Greeting { message: "hello" })
}
Register it with route("/greeting", get(greeting)). Axum serializes the
response through its JSON response type and sets the appropriate content type.
Extract a path and request body
use axum::{extract::Path, Json};
use serde::{Deserialize, Serialize};
#[derive(Deserialize)]
struct CreateNote {
body: String,
}
#[derive(Serialize)]
struct Note {
id: u64,
body: String,
}
async fn create_note(
Path(id): Path<u64>,
Json(input): Json<CreateNote>,
) -> Json<Note> {
Json(Note { id, body: input.body })
}
Attach it with post("/notes/{id}", create_note). Validate size, encoding,
authorization, and business rules before accepting real input. The example does
not persist data or authenticate a caller.
Share state deliberately
use axum::{extract::State, routing::get, Router};
use std::sync::Arc;
#[derive(Clone)]
struct AppState {
service_name: Arc<str>,
}
async fn about(State(state): State<AppState>) -> String {
format!("service={}", state.service_name)
}
fn router(state: AppState) -> Router {
Router::new().route("/about", get(about)).with_state(state)
}
State should contain handles and configuration with clear ownership. Keep request-specific data in the request, not in mutable global state.
Validate and model errors
An API should return a stable public error shape while logging only safe,
correlation-friendly details. A real implementation can define an error type
that implements Axum’s response conversion and maps validation failures to
400, missing resources to 404, and authorization failures to 401 or 403
according to its policy. Never return stack traces, secrets, SQL text, or raw
upstream responses to clients.
Request flow

- The router matches the method and path.
- Extractors parse path, query, headers, and body.
- Validation checks shape, size, and business constraints.
- Authorization checks the caller and requested resource.
- The handler calls an owned application service.
- The response maps success or a bounded public error.
This separation makes unit tests small and makes middleware responsibilities visible.
Common mistakes and troubleshooting
- Handler does not satisfy the route: check extractor order and return type.
- Body extraction fails: send valid JSON with the correct content type and enforce a body limit.
- State cannot be cloned: store an Arc or a cheap, cloneable handle rather than copying a connection or secret.
- Runtime blocks: do not perform blocking file or database work on the async executor; use an appropriate blocking boundary.
- Unexpected 404: verify method, path parameters, and router nesting.
Security and production considerations
Add authentication, authorization, CSRF or origin controls where applicable,
rate limits, body and header limits, TLS, timeouts, bounded concurrency,
structured redacted logs, graceful shutdown, and health/readiness separation.
Do not treat a local 200 OK as proof of production readiness.
Summary
Axum makes the route and extraction layers compact, but a reliable API still needs explicit validation, authorization, bounded resources, and safe error mapping. Keep the tutorial service local and extend it only after each boundary has a testable contract.
Next step and technical review flags
Continue with Database Access in Rust with SQLx. Confirm current Axum, Tokio, Tower, extractor, route-parameter, and server APIs against their official documentation before publication. Compile and exercise the complete example against the locked dependency versions.
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
Troubleshoot Kubernetes issues including Pending Pods, CrashLoopBackOff, ImagePullBackOff, service routing, in...
7 min read · Sep 17, 2026
Learn Kubernetes security best practices with RBAC, Secrets, ServiceAccounts, Pod securityContext, NetworkPoli...
6 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
Learn Linux from the ground up with simple explanations of the kernel, distributions, filesystem, commands, pe...
4 min read · Updated Oct 1, 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.