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 IntoResponse

Each 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 -> body

Multiple 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 processing

Use 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).