What is the difference between axum::response::IntoResponse and IntoResponseParts for composing HTTP responses?
IntoResponse converts a type into a complete HTTP response including body, while IntoResponseParts converts a type into response components like headers and status code that can be combined with other parts to build a complete response. IntoResponseParts enables composable response building where you can layer headers, status codes, and other metadata onto a body without manually constructing the full response.
IntoResponse Basics
use axum::{
response::{IntoResponse, Response},
http::StatusCode,
};
// IntoResponse converts a type to a complete HTTP response
async fn handler() -> impl IntoResponse {
"Hello, World!"
}
// Multiple types implement IntoResponse
async fn various_responses() {
// String body with default 200 OK
"text response";
// Status code only
StatusCode::NOT_FOUND;
// Tuple combines multiple IntoResponse types
(StatusCode::CREATED, "Created successfully");
// JSON body
axum::Json(serde_json::json!({ "status": "ok" }));
// Headers with body
(
[("content-type", "text/html")],
"<html><body>Hello</body></html>"
);
}IntoResponse is the trait for types that can become complete HTTP responses.
IntoResponseParts Basics
use axum::{
response::{IntoResponse, IntoResponseParts, ResponseParts},
http::{StatusCode, header},
};
// IntoResponseParts converts a type to response metadata (headers, status)
struct Created;
impl IntoResponseParts for Created {
fn into_response_parts(self, parts: ResponseParts) -> Result<ResponseParts, Self::Error> {
// Modify response parts
let mut parts = parts;
parts.status = StatusCode::CREATED;
parts.headers.insert(header::LOCATION, "/resource/123".parse().unwrap());
Ok(parts)
}
}
async fn handler() -> impl IntoResponse {
// Combine Created (IntoResponseParts) with body
(Created, "Resource created")
}IntoResponseParts builds components that can be combined with other response parts.
The Key Difference
use axum::{
response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
http::{StatusCode, HeaderMap},
};
// IntoResponse: Creates complete response from scratch
// - Must provide body
// - Must provide status (or use default)
// - Must provide headers (or use default)
impl IntoResponse for MyResponse {
fn into_response(self) -> Response {
let mut response = Response::new("body".into());
*response.status_mut() = StatusCode::OK;
response.headers_mut().insert("content-type", "text/plain".parse().unwrap());
response
}
}
// IntoResponseParts: Modifies existing response parts
// - Receives ResponseParts parameter
// - Adds/modifies headers
// - Changes status code
// - Does NOT provide body
impl IntoResponseParts for MyHeaders {
type Error = std::convert::Infallible;
fn into_response_parts(self, mut parts: ResponseParts) -> Result<ResponseParts, Self::Error> {
parts.headers.insert("x-custom", "value".parse().unwrap());
Ok(parts)
}
}
// The key distinction:
// - IntoResponse: "I am a complete response"
// - IntoResponseParts: "I am a modifier that adds to responses"IntoResponse creates; IntoResponseParts modifies.
Combining with Tuples
use axum::{
response::IntoResponse,
http::{StatusCode, header},
};
async fn handler() -> impl IntoResponse {
// Tuples allow composing multiple IntoResponseParts with a body
(
StatusCode::CREATED, // IntoResponseParts
[ (header::CONTENT_TYPE, "application/json") ], // IntoResponseParts
[ ("x-request-id", "abc123") ], // IntoResponseParts
r#"{"status": "created"}"#, // IntoResponse (body)
)
}
async fn auth_handler() -> impl IntoResponse {
(
StatusCode::OK,
[ (header::AUTHORIZATION, "Bearer token") ],
"Authenticated"
)
}
async fn cached_handler() -> impl IntoResponse {
(
StatusCode::OK,
[ (header::CACHE_CONTROL, "max-age=3600") ],
[ (header::ETAG, "\"abc123\"") ],
"Cached content"
)
}Tuples combine IntoResponseParts implementors with a final IntoResponse body.
How Tuples Build Responses
use axum::{
response::{IntoResponse, IntoResponseParts, ResponseParts},
http::StatusCode,
};
// When you write:
// (StatusCode::CREATED, "body")
//
// Axum builds the response like this:
fn build_response_example() {
// 1. Start with default response parts
let parts = ResponseParts::default();
// 2. Apply StatusCode::CREATED (IntoResponseParts)
let parts = StatusCode::CREATED.into_response_parts(parts).unwrap();
// 3. Apply body (IntoResponse)
// "body" implements IntoResponse, which creates the final response
// by combining the parts with the body
}
// The tuple composition works because:
// - StatusCode implements IntoResponseParts
// - String (&str) implements IntoResponse
// - (A, B) where A: IntoResponseParts, B: IntoResponse implements IntoResponseEach element in the tuple contributes parts to the final response.
Built-in IntoResponseParts Implementors
use axum::{
response::IntoResponse,
http::{StatusCode, HeaderMap, header},
};
async fn built_in_parts() -> impl IntoResponse {
// StatusCode implements IntoResponseParts
(
StatusCode::CREATED, // Sets status code
"Body content"
)
}
async fn headers_as_parts() -> impl IntoResponse {
// Header arrays implement IntoResponseParts
(
[ (header::CONTENT_TYPE, "application/json") ],
[ (header::CACHE_CONTROL, "no-cache") ],
r#"{"data": "value"}"#
)
}
async fn multiple_headers() -> impl IntoResponse {
// Multiple header arrays combine
(
StatusCode::OK,
[
(header::CONTENT_TYPE, "text/html"),
(header::CACHE_CONTROL, "public"),
],
"<html><body>Content</body></html>"
)
}
async fn header_map() -> impl IntoResponse {
// HeaderMap implements IntoResponseParts
let mut headers = HeaderMap::new();
headers.insert(header::CONTENT_TYPE, "text/plain".parse().unwrap());
headers.insert("x-custom", "value".parse().unwrap());
(headers, "Plain text response")
}StatusCode and header types implement IntoResponseParts for easy composition.
Custom IntoResponseParts Implementation
use axum::{
response::{IntoResponse, IntoResponseParts, ResponseParts},
http::{StatusCode, header},
};
// Custom type for CORS headers
struct CorsHeaders;
impl IntoResponseParts for CorsHeaders {
type Error = std::convert::Infallible;
fn into_response_parts(self, mut parts: ResponseParts) -> Result<ResponseParts, Self::Error> {
parts.headers.insert(
header::ACCESS_CONTROL_ALLOW_ORIGIN,
"*".parse().unwrap()
);
parts.headers.insert(
header::ACCESS_CONTROL_ALLOW_METHODS,
"GET, POST, OPTIONS".parse().unwrap()
);
parts.headers.insert(
header::ACCESS_CONTROL_ALLOW_HEADERS,
"content-type".parse().unwrap()
);
Ok(parts)
}
}
async fn api_handler() -> impl IntoResponse {
// Use custom IntoResponseParts in tuple
(
CorsHeaders,
StatusCode::OK,
r#"{"status": "ok"}"#
)
}
// Reusable CORS for multiple handlers
async fn get_handler() -> impl IntoResponse {
(CorsHeaders, "GET response")
}
async fn post_handler() -> impl IntoResponse {
(CorsHeaders, StatusCode::CREATED, "POST response")
}Custom IntoResponseParts types create reusable response modifiers.
IntoResponseParts for Request Tracking
use axum::{
response::{IntoResponse, IntoResponseParts, ResponseParts},
http::header,
};
struct RequestId(String);
impl IntoResponseParts for RequestId {
type Error = std::convert::Infallible;
fn into_response_parts(self, mut parts: ResponseParts) -> Result<ResponseParts, Self::Error> {
parts.headers.insert(
"x-request-id",
self.0.parse().unwrap()
);
Ok(parts)
}
}
async fn tracked_handler() -> impl IntoResponse {
(
RequestId("req-12345".to_string()),
"Response with tracking"
)
}
// Can combine with other parts
async fn complex_tracked_handler() -> impl IntoResponse {
(
RequestId("req-67890".to_string()),
StatusCode::OK,
[ (header::CONTENT_TYPE, "application/json") ],
r#"{"data": "value"}"#
)
}Add request IDs or other tracking headers through custom IntoResponseParts.
Error Handling in IntoResponseParts
use axum::{
response::{IntoResponse, IntoResponseParts, ResponseParts},
http::StatusCode,
};
struct ValidatedHeader(String);
impl IntoResponseParts for ValidatedHeader {
type Error = ValidationError;
fn into_response_parts(self, mut parts: ResponseParts) -> Result<ResponseParts, Self::Error> {
// Validate header value
if self.0.len() > 100 {
return Err(ValidationError::TooLong);
}
parts.headers.insert(
"x-validated",
self.0.parse().unwrap()
);
Ok(parts)
}
}
#[derive(Debug)]
enum ValidationError {
TooLong,
}
// When into_response_parts returns Err, the error is converted
// into a response (if Error implements IntoResponse)
async fn validated_handler() -> impl IntoResponse {
(
ValidatedHeader("valid-value".to_string()),
"Success"
)
}IntoResponseParts can validate and return errors during response construction.
Layering Response Parts
use axum::{
response::{IntoResponse, IntoResponseParts, ResponseParts},
http::{StatusCode, header},
};
// First layer: add security headers
struct SecurityHeaders;
impl IntoResponseParts for SecurityHeaders {
type Error = std::convert::Infallible;
fn into_response_parts(self, mut parts: ResponseParts) -> Result<ResponseParts, Self::Error> {
parts.headers.insert("x-frame-options", "DENY".parse().unwrap());
parts.headers.insert("x-content-type-options", "nosniff".parse().unwrap());
Ok(parts)
}
}
// Second layer: add caching
struct CacheHeaders;
impl IntoResponseParts for CacheHeaders {
type Error = std::convert::Infallible;
fn into_response_parts(self, mut parts: ResponseParts) -> Result<ResponseParts, Self::Error> {
parts.headers.insert(header::CACHE_CONTROL, "public, max-age=3600".parse().unwrap());
Ok(parts)
}
}
async fn layered_handler() -> impl IntoResponse {
(
SecurityHeaders,
CacheHeaders,
StatusCode::OK,
"Protected and cached response"
)
}
// Order matters: parts are applied left to right
// SecurityHeaders -> CacheHeaders -> StatusCode -> bodyMultiple IntoResponseParts can be layered to build complex responses.
IntoResponse vs IntoResponseParts Trade-offs
use axum::{
response::{IntoResponse, IntoResponseParts, Response, ResponseParts},
http::{StatusCode, HeaderMap, header},
};
// IntoResponse: Use when you have a complete response
impl IntoResponse for ApiResponse {
fn into_response(self) -> Response {
// Must build entire response
let mut response = Response::new(self.body.into());
*response.status_mut() = self.status;
for (key, value) in self.headers {
response.headers_mut().insert(key, value);
}
response
}
}
// IntoResponseParts: Use for composable modifiers
impl IntoResponseParts for ApiHeaders {
type Error = std::convert::Infallible;
fn into_response_parts(self, mut parts: ResponseParts) -> Result<ResponseParts, Self::Error> {
// Just add headers, don't worry about body
parts.headers.insert("x-api-version", "1.0".parse().unwrap());
Ok(parts)
}
}
// When to use which:
// - IntoResponse: When type IS the response (Json, Html, String)
// - IntoResponseParts: When type MODIFIES responses (headers, status, middleware-like)Use IntoResponse for complete responses, IntoResponseParts for modifiers.
Practical Example: JSON API
use axum::{
response::{IntoResponse, IntoResponseParts, ResponseParts},
http::{StatusCode, header},
Json,
};
// Reusable API headers
struct ApiHeaders;
impl IntoResponseParts for ApiHeaders {
type Error = std::convert::Infallible;
fn into_response_parts(self, mut parts: ResponseParts) -> Result<ResponseParts, Self::Error> {
parts.headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap());
parts.headers.insert("x-api-version", "1.0".parse().unwrap());
Ok(parts)
}
}
// Success responses
async fn get_users() -> impl IntoResponse {
(
ApiHeaders,
StatusCode::OK,
Json(vec!["user1", "user2"])
)
}
async fn create_user() -> impl IntoResponse {
(
ApiHeaders,
StatusCode::CREATED,
[ (header::LOCATION, "/users/123") ],
Json(serde_json::json!({ "id": 123 }))
)
}
// Error responses
async fn not_found() -> impl IntoResponse {
(
ApiHeaders,
StatusCode::NOT_FOUND,
Json(serde_json::json!({ "error": "Not found" }))
)
}Compose JSON API responses from reusable parts.
Response Extensions with IntoResponseParts
use axum::{
response::{IntoResponse, IntoResponseParts, ResponseParts},
http::StatusCode,
};
// Add data to response extensions
struct TimingData {
duration_ms: u64,
}
impl IntoResponseParts for TimingData {
type Error = std::convert::Infallible;
fn into_response_parts(self, mut parts: ResponseParts) -> Result<ResponseParts, Self::Error> {
// Extensions can be used by middleware or other parts
parts.extensions.insert(self);
Ok(parts)
}
}
async fn timed_handler() -> impl IntoResponse {
(
TimingData { duration_ms: 42 },
StatusCode::OK,
"Response with timing data"
)
}
// Extensions are accessible in middleware or during response processingUse IntoResponseParts to add extensions to responses for middleware.
Synthesis
Quick reference:
| Trait | Purpose | Produces |
|---|---|---|
IntoResponse |
Complete response | Full Response with body |
IntoResponseParts |
Response modifier | ResponseParts without body |
Composition pattern:
use axum::{
response::IntoResponse,
http::{StatusCode, header},
};
async fn handler() -> impl IntoResponse {
// Tuple structure: (parts..., body)
// - All but last element: IntoResponseParts
// - Last element: IntoResponse
(
// IntoResponseParts: StatusCode
StatusCode::CREATED,
// IntoResponseParts: headers
[ (header::CONTENT_TYPE, "application/json") ],
// IntoResponse: body (last element)
r#"{"status": "created"}"#
)
}Key insight: IntoResponse and IntoResponseParts work together through tuple composition to build HTTP responses. IntoResponse creates complete responsesāit's implemented by types that represent response bodies like String, Json<T>, and Html<T>. IntoResponseParts modifies response metadataāit's implemented by types that contribute headers, status codes, or extensions without providing a body. The magic happens with tuples: when you write (StatusCode::CREATED, "body"), Axum applies the StatusCode::CREATED as IntoResponseParts to modify the response parts, then uses "body" as IntoResponse to provide the body. This composition continues for longer tuplesā(A, B, C, body) applies A, B, and C as IntoResponseParts modifiers before using body as the response content. This pattern enables building complex responses from simple, reusable components: create a custom CorsHeaders type implementing IntoResponseParts, then use it across all your handlers with (CorsHeaders, your_body). Implement IntoResponse for types that ARE responses (like Json<T> or Html<T>), and implement IntoResponseParts for types that MODIFY responses (like custom headers, CORS policies, or status code wrappers).
