Rust AxumBackend DevelopmentMiddlewareREST APIRust

Article & Tutorial

Build a REST API with Rust and Axum: Routes, JSON, State and Middleware

Build a practical REST API with Rust and Axum, including routing, JSON handlers, shared state, middleware, request IDs, errors, validation and graceful shutdown.

Build a REST API with Rust and Axum: Routes, JSON, State and Middleware article illustration
Build a REST API with Rust and Axum: Routes, JSON, State and Middleware article illustration
Contents

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

bash
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 uuid

2. Start a minimal server

bash
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

bash
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

bash
use axum::extract::Path; async fn get_user(Path(id): Path<u64>) -> String { format!("user {id}") }

5. Query parameters

bash
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

bash
#[derive(serde::Deserialize)] struct CreateUser { name: String } async fn create(Json(input): Json<CreateUser>) -> Json<CreateUser> { Json(input) }

7. Application state

bash
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

bash
src/ ├── main.rs ├── app.rs ├── config.rs ├── error.rs ├── routes/ │ ├── mod.rs │ ├── health.rs │ └── users.rs ├── services/ └── repositories/

9. HTTP status codes

bash
use axum::http::StatusCode; async fn create_user() -> (StatusCode, &'static str) { (StatusCode::CREATED, "created") }

10. Central application errors

bash
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

bash
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

bash
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.

Did this help?

Yes No

Join the discussion

0 Comments

Ask questions, add context, or share what worked.

Sign in to join the discussion

No comments yet. Start the discussion.

More from Melochip Editorial

Related articles

Stay in the loop. Get new Melochip articles

Receive Melochip article updates when new articles are available. Explicit opt-in only.

No pre-checked boxes. Unsubscribe support is part of the subscriber foundation.