What is the difference between axum::response::IntoResponse::into_response and IntoResponseParts for constructing HTTP responses?
IntoResponse::into_response creates a complete Response from a type, while IntoResponseParts allows types to contribute response parts (headers, extensions, status) to be combined with other parts into a final response. into_response is the primary mechanism for returning values from handlersâthe trait converts any type implementing it into a full HTTP response. IntoResponseParts is a supporting trait that enables composable response construction, letting you combine status codes, headers, and body content using tuple syntax. This separation allows types like StatusCode, HeaderMap, and body types to independently define their response contributions.
Basic into_response Usage
use axum::{
response::IntoResponse,
http::StatusCode,
Json,
};
use serde_json::json;
async fn basic_handler() -> impl IntoResponse {
// String implements IntoResponse
"Hello, World!"
}
async fn json_handler() -> impl IntoResponse {
// Json<T> implements IntoResponse
Json(json!({ "message": "hello" }))
}
async fn status_handler() -> impl IntoResponse {
// StatusCode implements IntoResponse
StatusCode::OK
}into_response converts the return type into a complete Response.
The IntoResponse Trait
use axum::response::IntoResponse;
use axum::http::Response;
// Simplified trait definition:
// pub trait IntoResponse {
// fn into_response(self) -> Response<axum::body::Body>;
// }
fn trait_explanation() {
// Any type implementing IntoResponse can be returned from handlers
// The into_response method creates a complete Response
// Built-in implementations:
// - String, &str
// - Vec<u8>, &[u8]
// - Json<T>
// - StatusCode
// - ()
// - Result<T, E> where T: IntoResponse
// - (StatusCode, T) where T: IntoResponse
// - (Headers, T) where T: IntoResponse
// - (StatusCode, Headers, T) where T: IntoResponse
}IntoResponse::into_response returns a complete Response<Body>.
IntoResponseParts for Composable Responses
use axum::response::{IntoResponse, IntoResponseParts, ResponseParts};
use axum::http::{StatusCode, HeaderMap, header};
// Simplified trait definition:
// pub trait IntoResponseParts {
// fn into_response_parts(self, res: ResponseParts) -> ResponseParts;
// }
// Types implementing IntoResponseParts can be combined:
// - StatusCode
// - HeaderMap
// - Headers (from axum::headers crate)
// - Extensions
async fn combined_response() -> impl IntoResponse {
// Tuple syntax combines IntoResponseParts implementors
(
StatusCode::CREATED,
[("content-type", "application/json")],
r#"{"status":"ok"}"#,
)
}IntoResponseParts types can be combined in tuples to build responses.
Combining Status with Body
use axum::{response::IntoResponse, http::StatusCode};
async fn status_with_body() -> impl IntoResponse {
// StatusCode implements IntoResponseParts
// String implements IntoResponse
// Together they form a complete response
(StatusCode::CREATED, "Created successfully")
}
async fn error_response() -> impl IntoResponse {
(StatusCode::BAD_REQUEST, "Invalid request")
}
async fn not_found() -> impl IntoResponse {
(StatusCode::NOT_FOUND, "Resource not found")
}Status codes combine with body content using tuples.
Adding Headers to Responses
use axum::{
response::IntoResponse,
http::{StatusCode, HeaderMap, header},
};
async fn with_headers() -> impl IntoResponse {
// Multiple headers in a tuple
(
StatusCode::OK,
[
(header::CONTENT_TYPE, "text/plain"),
(header::CACHE_CONTROL, "max-age=3600"),
],
"Cached content",
)
}
async fn with_header_map() -> impl IntoResponse {
// HeaderMap implements IntoResponseParts
let mut headers = HeaderMap::new();
headers.insert(header::CONTENT_TYPE, "text/html".parse().unwrap());
headers.insert(header::X_CUSTOM_HEADER, "value".parse().unwrap());
(StatusCode::OK, headers, "<html>...</html>")
}Headers can be added as tuples of tuples or as HeaderMap.
Full Response Construction
use axum::{
response::IntoResponse,
http::{StatusCode, HeaderMap, header},
};
async fn full_response() -> impl IntoResponse {
// Status + Headers + Body
(
StatusCode::OK,
[(header::CONTENT_TYPE, "application/json")],
r#"{"data": "value"}"#,
)
}
async fn full_response_explicit() -> impl IntoResponse {
// Same thing, more explicit
let status = StatusCode::OK;
let mut headers = HeaderMap::new();
headers.insert(header::CONTENT_TYPE, "application/json".parse().unwrap());
let body = r#"{"data": "value"}"#;
(status, headers, body)
}Combine status, headers, and body in a tuple for complete control.
Implementing IntoResponse for Custom Types
use axum::{
response::{IntoResponse, Response},
http::{StatusCode, header},
};
struct ApiResponse<T> {
data: T,
status: StatusCode,
}
impl<T: serde::Serialize> IntoResponse for ApiResponse<T> {
fn into_response(self) -> Response {
// Build response from scratch
let body = serde_json::to_string(&self.data).unwrap();
Response::builder()
.status(self.status)
.header(header::CONTENT_TYPE, "application/json")
.body(body.into())
.unwrap()
}
}
async fn custom_type() -> impl IntoResponse {
ApiResponse {
data: vec!["a", "b", "c"],
status: StatusCode::OK,
}
}Implement IntoResponse for custom types to control response generation.
Implementing IntoResponseParts for Custom Types
use axum::{
response::{IntoResponse, IntoResponseParts, ResponseParts},
http::{StatusCode, HeaderMap},
};
// A custom type that adds headers to any response
struct CacheHeaders {
max_age: u64,
}
impl IntoResponseParts for CacheHeaders {
fn into_response_parts(self, mut parts: ResponseParts) -> ResponseParts {
// Modify the response parts
parts.headers.insert(
axum::http::header::CACHE_CONTROL,
format!("max-age={}", self.max_age).parse().unwrap(),
);
parts
}
}
impl IntoResponse for CacheHeaders {
fn into_response(self) -> axum::response::Response {
// When used alone, just adds headers to empty response
self.into_response_parts(ResponseParts::default()).into_response()
}
}
async fn with_cache_headers() -> impl IntoResponse {
// CacheHeaders contributes headers, string provides body
(
CacheHeaders { max_age: 3600 },
StatusCode::OK,
"Cached content",
)
}Implement IntoResponseParts to contribute headers or extensions.
How Tuple Combination Works
use axum::response::{IntoResponse, IntoResponseParts};
// When you return a tuple like (StatusCode, String):
// 1. StatusCode implements IntoResponseParts
// 2. String implements IntoResponse
// 3. The tuple (A, B) where A: IntoResponseParts, B: IntoResponse implements IntoResponse
// The generated implementation roughly does:
// fn into_response(self) -> Response {
// let (parts, body) = self;
// let response = body.into_response();
// parts.into_response_parts(response.into_parts())
// }
async fn tuple_ordering() -> impl IntoResponse {
// Order matters for readability but all combinations work:
// Status + Body
(StatusCode::OK, "body")
// Headers + Body
([("x-custom", "value")], "body")
// Status + Headers + Body
(StatusCode::OK, [("x-custom", "value")], "body")
}Tuples combine IntoResponseParts implementors with an IntoResponse body.
Extensions with IntoResponseParts
use axum::{
response::{IntoResponse, IntoResponseParts, ResponseParts},
http::StatusCode,
extract::Extension,
};
struct RequestId(String);
impl IntoResponseParts for RequestId {
fn into_response_parts(self, mut parts: ResponseParts) -> ResponseParts {
// Add extension to response
parts.extensions.insert(self);
parts
}
}
async fn with_extension() -> impl IntoResponse {
(
RequestId("req-123".to_string()),
StatusCode::OK,
"Response with request ID extension",
)
}IntoResponseParts can add extensions to responses for downstream processing.
Response Parts Structure
use axum::response::ResponseParts;
use axum::http::{StatusCode, HeaderMap, Extensions};
// ResponseParts contains everything except the body:
// pub struct ResponseParts {
// pub status: StatusCode,
// pub version: axum::http::Version,
// pub headers: HeaderMap,
// pub extensions: Extensions,
// }
// IntoResponseParts receives mutable access to these
// and can modify them before the body is attached
fn response_parts_example() {
// ResponseParts is created from a Response
// Parts implementors modify it
// Then it's combined back with the body
}ResponseParts contains status, version, headers, and extensions.
Common Patterns
use axum::{
response::IntoResponse,
http::StatusCode,
Json,
};
use serde_json::json;
// Pattern 1: Simple body (default status)
async fn simple() -> impl IntoResponse {
"Just a body" // StatusCode::OK is default
}
// Pattern 2: Status + body
async fn with_status() -> impl IntoResponse {
(StatusCode::CREATED, "Created")
}
// Pattern 3: Status + headers + body
async fn full_control() -> impl IntoResponse {
(
StatusCode::OK,
[("x-request-id", "123")],
Json(json!({ "data": "value" })),
)
}
// Pattern 4: Result handling
async fn result_handling() -> impl IntoResponse {
let result: Result<String, String> = Ok("success".to_string());
// Result<T, E> where T: IntoResponse, E: IntoResponse
result
}
// Pattern 5: Custom error type
async fn custom_error() -> impl IntoResponse {
let result: Result<String, AppError> = Err(AppError::NotFound);
// If AppError implements IntoResponse
result
}
enum AppError {
NotFound,
InternalError,
}
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
match self {
AppError::NotFound => (StatusCode::NOT_FOUND, "Not found").into_response(),
AppError::InternalError => (StatusCode::INTERNAL_SERVER_ERROR, "Internal error").into_response(),
}
}
}These patterns cover most response construction needs.
Difference Between Traits
use axum::response::{IntoResponse, IntoResponseParts};
fn trait_comparison() {
// IntoResponse:
// - Creates a COMPLETE Response
// - Has body, status, headers, extensions
// - Used as final return value from handlers
// - Types: String, Vec<u8>, Json<T>, (), etc.
// IntoResponseParts:
// - MODIFIES an existing ResponseParts
// - No body (received from context)
// - Used as component in tuple responses
// - Types: StatusCode, HeaderMap, Extensions
}IntoResponse creates complete responses; IntoResponseParts modifies parts.
Type Implementations
use axum::response::{IntoResponse, IntoResponseParts};
fn implementations() {
// Types implementing IntoResponse (complete responses):
// - String, &str
// - Vec<u8>, &[u8]
// - Json<T>
// - Html<T>
// - StatusCode (as full response)
// - ()
// - Result<T, E> where T, E: IntoResponse
// - (T,) where T: IntoResponse
// - (A, B) where A: IntoResponseParts, B: IntoResponse
// - (A, B, C) where A, B: IntoResponseParts, C: IntoResponse
// - etc.
// Types implementing IntoResponseParts (response parts):
// - StatusCode
// - HeaderMap
// - [(HeaderName,HeaderValue); N]
// - Option<T> where T: IntoResponseParts
// - Extensions
}IntoResponse has many implementors; IntoResponseParts has status and headers.
Middleware and IntoResponseParts
use axum::{
response::{IntoResponse, IntoResponseParts, ResponseParts},
http::StatusCode,
};
// Useful for middleware that adds headers
struct SecurityHeaders;
impl IntoResponseParts for SecurityHeaders {
fn into_response_parts(self, mut parts: ResponseParts) -> ResponseParts {
parts.headers.insert(
axum::http::header::HeaderName::from_static("x-frame-options"),
"DENY".parse().unwrap(),
);
parts.headers.insert(
axum::http::header::HeaderName::from_static("x-content-type-options"),
"nosniff".parse().unwrap(),
);
parts
}
}
async fn secure_response() -> impl IntoResponse {
// Security headers added automatically
(SecurityHeaders, "Safe content")
}IntoResponseParts is useful for reusable response components.
Practical Example: API Responses
use axum::{
response::{IntoResponse, IntoResponseParts, ResponseParts},
http::StatusCode,
Json,
};
use serde_json::json;
struct Created;
struct NoContent;
impl IntoResponseParts for Created {
fn into_response_parts(self, mut parts: ResponseParts) -> ResponseParts {
parts.status = StatusCode::CREATED;
parts
}
}
impl IntoResponseParts for NoContent {
fn into_response_parts(self, mut parts: ResponseParts) -> ResponseParts {
parts.status = StatusCode::NO_CONTENT;
parts
}
}
async fn create_item() -> impl IntoResponse {
(
Created,
Json(json!({ "id": 1, "created": true })),
)
}
async fn delete_item() -> impl IntoResponse {
(NoContent, ())
}Custom IntoResponseParts types create readable, reusable response patterns.
Comparison Summary
use axum::response::{IntoResponse, IntoResponseParts};
fn comparison_table() {
// | Aspect | IntoResponse | IntoResponseParts |
// |--------|--------------|-------------------|
// | Output | Complete Response | Modified ResponseParts |
// | Body | Includes body | No body |
// | Usage | Final return type | Component in tuple |
// | Examples | String, Json<T>, () | StatusCode, HeaderMap |
// | Composition | Can combine with Parts | Combines into Response |
// Key difference:
// - IntoResponse: "I am a complete response"
// - IntoResponseParts: "I add something to a response"
}Synthesis
Quick reference:
use axum::{
response::{IntoResponse, IntoResponseParts},
http::StatusCode,
Json,
};
async fn quick_reference() {
// IntoResponse: creates complete response
let _: axum::response::Response = "body".into_response();
// IntoResponseParts: modifies response parts
// Used in tuples to combine components
// Common combinations:
// Body only (default status):
let _ = "body".into_response();
// Status + body:
(StatusCode::CREATED, "body")
// Headers + body:
([("x-custom", "value")], "body")
// Status + headers + body:
(StatusCode::OK, [("x-custom", "value")], "body")
// Custom IntoResponseParts:
struct CustomHeader;
impl IntoResponseParts for CustomHeader {
fn into_response_parts(self, mut parts: axum::response::ResponseParts) -> axum::response::ResponseParts {
parts.headers.insert(axum::http::header::HeaderName::from_static("x-custom"), "value".parse().unwrap());
parts
}
}
(CustomHeader, "body")
}Key insight: The separation between IntoResponse and IntoResponseParts enables axum's composable response system. IntoResponse::into_response is the fundamental conversion traitâevery type that can be returned from a handler implements it, producing a complete Response. IntoResponseParts serves as a building block trait, allowing types like StatusCode and HeaderMap to define their contribution to a response. The tuple implementation (A, B) where A: IntoResponseParts, B: IntoResponse ties them together, first converting the body into a response, then modifying its parts with each IntoResponseParts implementor. This design allows expressive response construction like (StatusCode::CREATED, [("location", "/items/1")], Json(item)) while keeping each component's implementation focused on its specific concern. Custom IntoResponseParts implementations enable reusable response modificationsâadding security headers, request IDs, or timing information across many handlers without repeating code.
