Skip to main content
In Axum, any type that implements the IntoResponse trait can be returned from a handler. This provides a flexible and type-safe way to construct HTTP responses.

The IntoResponse trait

The IntoResponse trait converts types into HTTP responses:
use axum::response::{IntoResponse, Response};

pub trait IntoResponse {
    fn into_response(self) -> Response;
}
Many types implement this trait out of the box.

Basic response types

String types return plain text responses:
use axum::{Router, routing::get};

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

async fn dynamic() -> String {
    format!("The time is: {:?}", std::time::SystemTime::now())
}

let app = Router::new()
    .route("/static", get(handler))
    .route("/dynamic", get(dynamic));
Returns Content-Type: text/plain; charset=utf-8

JSON responses

Return JSON using the Json wrapper:
use axum::{Router, routing::get, Json};
use serde::{Serialize, Deserialize};

#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
}

async fn get_user() -> Json<User> {
    Json(User {
        id: 1,
        name: "Alice".to_string(),
    })
}

let app = Router::new()
    .route("/users/1", get(get_user));
Returns Content-Type: application/json

Tuple responses

Combine status codes, headers, and bodies using tuples:
use axum::{
    Router,
    routing::{get, post},
    http::{StatusCode, HeaderMap, header},
    Json,
};
use serde::Serialize;

#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
}

// Status + Body
async fn create_user() -> (StatusCode, Json<User>) {
    (
        StatusCode::CREATED,
        Json(User { id: 1, name: "Alice".to_string() })
    )
}

// Status + Headers + Body
async fn handler_with_headers() -> (StatusCode, HeaderMap, &'static str) {
    let mut headers = HeaderMap::new();
    headers.insert(header::CACHE_CONTROL, "max-age=3600".parse().unwrap());
    
    (StatusCode::OK, headers, "Cached response")
}

// Headers + Body (status defaults to 200)
async fn with_headers() -> (HeaderMap, &'static str) {
    let mut headers = HeaderMap::new();
    headers.insert(header::CONTENT_TYPE, "text/plain".parse().unwrap());
    
    (headers, "Hello")
}

let app = Router::new()
    .route("/users", post(create_user))
    .route("/cached", get(handler_with_headers))
    .route("/headers", get(with_headers));

Custom headers

Add headers to responses:
use axum::{
    http::{StatusCode, header},
    response::IntoResponse,
};

async fn handler() -> impl IntoResponse {
    (
        StatusCode::OK,
        [(header::CONTENT_TYPE, "text/plain")],
        "Hello, World!"
    )
}

Redirects

Redirect to different URLs:
use axum::{
    Router,
    routing::get,
    response::Redirect,
};

// Temporary redirect (302)
async fn old_route() -> Redirect {
    Redirect::temporary("/new-route")
}

// Permanent redirect (301)
async fn moved() -> Redirect {
    Redirect::permanent("/new-location")
}

// See Other redirect (303)
async fn after_post() -> Redirect {
    Redirect::to("/success")
}

let app = Router::new()
    .route("/old", get(old_route))
    .route("/moved", get(moved))
    .route("/redirect", get(after_post));

Result types

Use Result for error handling:
use axum::{
    Router,
    routing::get,
    http::StatusCode,
    Json,
};
use serde::Serialize;

#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
}

async fn get_user() -> Result<Json<User>, StatusCode> {
    let user = fetch_user_from_db().await;
    
    match user {
        Some(user) => Ok(Json(user)),
        None => Err(StatusCode::NOT_FOUND),
    }
}

async fn fetch_user_from_db() -> Option<User> {
    Some(User { id: 1, name: "Alice".to_string() })
}

let app = Router::new()
    .route("/users/:id", get(get_user));
Both the Ok and Err types must implement IntoResponse.

Custom response types

Implement IntoResponse for your own types:
use axum::{
    response::{IntoResponse, Response},
    http::StatusCode,
    Json,
};
use serde::Serialize;

#[derive(Serialize)]
struct ErrorResponse {
    message: String,
    code: u16,
}

enum AppError {
    NotFound,
    Unauthorized,
    InternalError,
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            AppError::NotFound => (
                StatusCode::NOT_FOUND,
                "Resource not found",
            ),
            AppError::Unauthorized => (
                StatusCode::UNAUTHORIZED,
                "Unauthorized access",
            ),
            AppError::InternalError => (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Internal server error",
            ),
        };

        let body = Json(ErrorResponse {
            message: message.to_string(),
            code: status.as_u16(),
        });

        (status, body).into_response()
    }
}

// Use in handlers
async fn handler() -> Result<&'static str, AppError> {
    if !is_authorized() {
        return Err(AppError::Unauthorized);
    }
    Ok("Success")
}

fn is_authorized() -> bool { true }

Response builders

For full control, build responses manually:
use axum::{
    response::{IntoResponse, Response},
    http::{StatusCode, header},
    body::Body,
};

async fn custom_response() -> Response {
    Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, "text/html")
        .header(header::CACHE_CONTROL, "max-age=3600")
        .body(Body::from("<h1>Custom Response</h1>"))
        .unwrap()
}

Streaming responses

Stream events to clients:
use axum::{
    Router,
    routing::get,
    response::sse::{Event, Sse},
};
use futures::stream::{self, Stream};
use std::{convert::Infallible, time::Duration};
use tokio_stream::StreamExt as _;

async fn sse_handler() -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
    let stream = stream::repeat_with(|| Event::default().data("hi"))
        .map(Ok)
        .throttle(Duration::from_secs(1));

    Sse::new(stream)
}

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

Response parts

Modify response parts separately:
use axum::{
    response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
    http::StatusCode,
};

struct CustomHeader;

impl IntoResponseParts for CustomHeader {
    type Error = (StatusCode, String);

    fn into_response_parts(
        self,
        mut res: ResponseParts
    ) -> Result<ResponseParts, Self::Error> {
        res.headers_mut().insert(
            "x-custom-header",
            "custom-value".parse().unwrap()
        );
        Ok(res)
    }
}

async fn handler() -> impl IntoResponse {
    (CustomHeader, "Response body")
}

Next steps

Error Handling

Learn advanced error handling patterns

Middleware

Transform requests and responses with middleware