IntoResponse trait can be returned from a handler. This provides a flexible and type-safe way to construct HTTP responses.
The IntoResponse trait
TheIntoResponse trait converts types into HTTP responses:
use axum::response::{IntoResponse, Response};
pub trait IntoResponse {
fn into_response(self) -> Response;
}
Basic response types
- Strings
- Status codes
- Empty responses
- HTML
String types return plain text responses:Returns
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));
Content-Type: text/plain; charset=utf-8Return HTTP status codes:
use axum::{
Router,
routing::{get, delete},
http::StatusCode,
};
async fn health() -> StatusCode {
StatusCode::OK
}
async fn delete_user() -> StatusCode {
// Delete user logic...
StatusCode::NO_CONTENT
}
let app = Router::new()
.route("/health", get(health))
.route("/users/:id", delete(delete_user));
Unit type returns empty 200 OK:
use axum::{Router, routing::post, http::StatusCode};
use axum::response::NoContent;
// Returns 200 OK with empty body
async fn handler_ok() {}
// Returns 204 No Content explicitly
async fn handler_no_content() -> NoContent {
NoContent
}
let app = Router::new()
.route("/ok", post(handler_ok))
.route("/no-content", post(handler_no_content));
Return HTML content:Returns
use axum::{Router, routing::get, response::Html};
async fn handler() -> Html<&'static str> {
Html("<h1>Hello, World!</h1>")
}
async fn dynamic_html() -> Html<String> {
Html(format!("<p>Generated at {:?}</p>", std::time::SystemTime::now()))
}
let app = Router::new()
.route("/", get(handler))
.route("/dynamic", get(dynamic_html));
Content-Type: text/html; charset=utf-8JSON responses
Return JSON using theJson 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));
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!"
)
}
use axum::{
response::{IntoResponse, AppendHeaders},
http::header,
};
async fn handler() -> impl IntoResponse {
(
AppendHeaders([
(header::CACHE_CONTROL, "max-age=3600"),
(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
UseResult 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
ImplementIntoResponse 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
Server-Sent Events (SSE)
Server-Sent Events (SSE)
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