Rust AxumJSONTokioweb APIs

Article & Tutorial

Building a JSON API with Rust and Axum

Build a small Axum service with routing, JSON extraction, shared state, validation, and explicit error responses.

Building a JSON API with Rust and Axum featured illustration
Building a JSON API with Rust and Axum featured illustration
Contents

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.

text
[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

rust
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

rust
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

rust
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

rust
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

Diagram showing a request passing through routing, validation, a handler, and a JSON response.
Keep transport, validation, authorization, and application logic explicit.
  1. The router matches the method and path.
  2. Extractors parse path, query, headers, and body.
  3. Validation checks shape, size, and business constraints.
  4. Authorization checks the caller and requested resource.
  5. The handler calls an owned application service.
  6. 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.

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.