Skip to main content
This guide will walk you through creating a simple HTTP server with Axum. You’ll learn the basics of routing, handlers, and serving your application.

Hello, World!

Let’s start with the simplest possible Axum application.
1

Create the project

If you haven’t already, create a new Rust project and add dependencies:
cargo new hello-axum
cd hello-axum
Update Cargo.toml:
[dependencies]
axum = "0.8.8"
tokio = { version = "1.0", features = ["full"] }
2

Write the server code

Replace the contents of src/main.rs with this code:
use axum::{response::Html, routing::get, Router};

#[tokio::main]
async fn main() {
    // Build our application with a route
    let app = Router::new().route("/", get(handler));

    // Run it
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    println!("listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, app).await;
}

async fn handler() -> Html<&'static str> {
    Html("<h1>Hello, World!</h1>")
}
3

Run the server

Start your server:
cargo run
You should see:
listening on 127.0.0.1:3000
4

Test the endpoint

Open your browser to http://localhost:3000 or use curl:
curl http://localhost:3000
You should see:
<h1>Hello, World!</h1>
The #[tokio::main] macro sets up the async runtime. All Axum handlers are async functions.

Understanding the code

Let’s break down what’s happening:

Router

let app = Router::new().route("/", get(handler));
The Router is the core of your application. You add routes using the .route() method, specifying:
  • The path ("/")
  • The HTTP method (get)
  • The handler function (handler)

Handler

async fn handler() -> Html<&'static str> {
    Html("<h1>Hello, World!</h1>")
}
Handlers are async functions that return something that implements IntoResponse. The Html type tells Axum to set the Content-Type header to text/html.

Server

let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
    .await
    .unwrap();
axum::serve(listener, app).await;
This creates a TCP listener and serves your application on port 3000.

Multiple routes

Let’s add more routes to handle different paths and HTTP methods:
use axum::{routing::{get, post}, http::StatusCode, Json, Router};
use serde::{Deserialize, Serialize};

#[tokio::main]
async fn main() {
    // Initialize tracing
    tracing_subscriber::fmt::init();

    // Build our application with multiple routes
    let app = Router::new()
        .route("/", get(root))
        .route("/users", post(create_user));

    // Run it
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    tracing::debug!("listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, app).await;
}

// Basic handler that responds with a static string
async fn root() -> &'static str {
    "Hello, World!"
}

async fn create_user(
    // This argument tells axum to parse the request body
    // as JSON into a `CreateUser` type
    Json(payload): Json<CreateUser>,
) -> (StatusCode, Json<User>) {
    // Insert your application logic here
    let user = User {
        id: 1337,
        username: payload.username,
    };

    // This will be converted into a JSON response
    // with a status code of `201 Created`
    (StatusCode::CREATED, Json(user))
}

// The input to our `create_user` handler
#[derive(Deserialize)]
struct CreateUser {
    username: String,
}

// The output to our `create_user` handler
#[derive(Serialize)]
struct User {
    id: u64,
    username: String,
}
Add serde = { version = "1.0", features = ["derive"] } and tracing-subscriber = "0.3" to your dependencies to run this example.

Testing the JSON endpoint

Test the POST endpoint with curl:
curl -X POST http://localhost:3000/users \
  -H "Content-Type: application/json" \
  -d '{"username": "alice"}'
Response:
{
  "id": 1337,
  "username": "alice"
}

Working with extractors

Axum provides extractors to parse different parts of requests:
use axum::Json;
use serde::Deserialize;

#[derive(Deserialize)]
struct Input {
    name: String,
}

async fn handler(Json(payload): Json<Input>) -> String {
    format!("Hello, {}!", payload.name)
}

Response types

Handlers can return different response types:
async fn handler() -> &'static str {
    "Hello, World!"
}

Adding middleware

Use Tower middleware to add cross-cutting concerns:
use tower_http::trace::TraceLayer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

#[tokio::main]
async fn main() {
    // Initialize tracing
    tracing_subscriber::registry()
        .with(
            tracing_subscriber::EnvFilter::try_from_default_env()
                .unwrap_or_else(|_| "info".into()),
        )
        .with(tracing_subscriber::fmt::layer())
        .init();

    let app = Router::new()
        .route("/", get(handler))
        // Add middleware
        .layer(TraceLayer::new_for_http());

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    tracing::info!("listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, app).await;
}

async fn handler() -> &'static str {
    "Hello, World!"
}
Add tower-http = { version = "0.6", features = ["trace"] } to use TraceLayer.

What’s next?

You now have a working Axum application! Here are some next steps:

Routing

Learn about advanced routing patterns and path parameters

Extractors

Deep dive into request extractors and validation

State management

Share application state across handlers

Middleware

Add authentication, logging, and other cross-cutting concerns

Full example

Here’s the complete example from this guide:
use axum::{
    routing::{get, post},
    http::StatusCode,
    Json, Router,
};
use serde::{Deserialize, Serialize};

#[tokio::main]
async fn main() {
    // Initialize tracing
    tracing_subscriber::fmt::init();

    // Build our application with a route
    let app = Router::new()
        // `GET /` goes to `root`
        .route("/", get(root))
        // `POST /users` goes to `create_user`
        .route("/users", post(create_user));

    // Run our app with hyper
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    tracing::debug!("listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, app).await;
}

// Basic handler that responds with a static string
async fn root() -> &'static str {
    "Hello, World!"
}

async fn create_user(
    // This argument tells axum to parse the request body
    // as JSON into a `CreateUser` type
    Json(payload): Json<CreateUser>,
) -> (StatusCode, Json<User>) {
    // Insert your application logic here
    let user = User {
        id: 1337,
        username: payload.username,
    };

    // This will be converted into a JSON response
    // with a status code of `201 Created`
    (StatusCode::CREATED, Json(user))
}

// The input to our `create_user` handler
#[derive(Deserialize)]
struct CreateUser {
    username: String,
}

// The output to our `create_user` handler
#[derive(Serialize)]
struct User {
    id: u64,
    username: String,
}
Find more complete examples in the Axum examples directory.