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
Strings
Status codes
Empty responses
HTML
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 Return 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: 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 ));
Returns Content-Type: text/html; 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 ));
Add headers to responses:
Using tuples
Using AppendHeaders
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