Skip to main content
Handlers are async functions that process HTTP requests and produce responses. They form the core logic of your application.

Basic handlers

A handler is any async function that implements the Handler trait:
use axum::{Router, routing::get};

// Handler that returns an empty 200 OK response
async fn unit_handler() {}

// Handler that returns a plain text response
async fn string_handler() -> String {
    "Hello, World!".to_string()
}

let app = Router::new()
    .route("/", get(unit_handler))
    .route("/hello", get(string_handler));

Handler signatures

Handlers can take extractors as arguments and return any type that implements IntoResponse:
1

Zero or more extractors

Extractors pull data from the request. They must implement FromRequestParts or FromRequest.
use axum::{extract::Path, http::HeaderMap};

async fn handler(
    Path(id): Path<u32>,
    headers: HeaderMap,
) -> String {
    format!("ID: {}, Headers: {:?}", id, headers)
}
2

Return type implements IntoResponse

The return type must be convertible to a response.
use axum::http::StatusCode;

// Returns just a status code
async fn handler() -> StatusCode {
    StatusCode::OK
}

// Returns a tuple (status, body)
async fn handler_with_status() -> (StatusCode, &'static str) {
    (StatusCode::CREATED, "Resource created")
}
3

Must be async

All handlers must be async functions.
// ✅ Correct
async fn handler() -> &'static str {
    "response"
}

// ❌ Won't compile - not async
fn handler() -> &'static str {
    "response"
}

Extractors in handlers

Handlers can extract data from requests using extractors:
use axum::{
    extract::{Path, Query},
    body::Bytes,
    http::StatusCode,
};
use serde::Deserialize;

#[derive(Deserialize)]
struct Pagination {
    page: usize,
    per_page: usize,
}

async fn handler(
    Path(user_id): Path<u32>,
    Query(pagination): Query<Pagination>,
    body: Bytes,
) -> Result<String, StatusCode> {
    if let Ok(body_str) = String::from_utf8(body.to_vec()) {
        Ok(format!(
            "User {}, page {}, body: {}",
            user_id, pagination.page, body_str
        ))
    } else {
        Err(StatusCode::BAD_REQUEST)
    }
}
The order of extractors matters. See Extractors for details.

Return types

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

async fn dynamic_handler() -> String {
    format!("Time: {}", std::time::SystemTime::now())
}

Error handling in handlers

Use Result types to handle errors:
use axum::{
    extract::Json,
    http::StatusCode,
    response::IntoResponse,
};
use serde::Deserialize;

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

async fn create_user(
    Json(payload): Json<CreateUser>,
) -> Result<String, StatusCode> {
    if payload.name.is_empty() {
        return Err(StatusCode::BAD_REQUEST);
    }

    Ok(format!("Created user: {}", payload.name))
}
For more complex error handling, see Error Handling.

Converting handlers to services

Handlers can be converted to Tower services:
use axum::handler::HandlerWithoutStateExt;

async fn handler() -> &'static str {
    "Hello"
}

let service = handler.into_service();

Applying middleware to handlers

You can apply middleware directly to individual handlers:
use axum::{
    Router,
    routing::get,
    handler::Handler,
};
use tower::limit::ConcurrencyLimitLayer;

async fn handler() -> &'static str {
    "Hello"
}

let layered_handler = handler.layer(ConcurrencyLimitLayer::new(64));

let app = Router::new()
    .route("/", get(layered_handler));

Debugging handler errors

Use the #[axum::debug_handler] macro to get better error messages during development:
use axum::debug_handler;

#[debug_handler]
async fn handler() -> String {
    "Hello".to_string()
}
This macro provides clearer compilation errors when your handler signature is incorrect.

Next steps

Extractors

Learn how to extract data from requests

Responses

Explore different response types