Crafting Custom Extractors in Axum and Actix Web
Min-jun Kim
Dev Intern · Leapcell

Unlocking Request Data with Custom Extractors
In the world of Rust web development, extracting specific pieces of information from incoming HTTP requests is a fundamental and frequently performed task. Whether it's user authentication tokens, parsed JSON bodies, or query parameters, the ability to cleanly and ergonomically access this data significantly impacts the readability and maintainability of your application's handlers. Both Axum and Actix Web, two popular web frameworks in Rust, provide powerful mechanisms to achieve this: the FromRequest trait. This trait is the cornerstone for building custom extractors, allowing developers to encapsulate complex data extraction logic into a reusable and idiomatic form. Understanding and leveraging FromRequest empowers you to write cleaner, more robust web APIs, moving beyond boilerplate and towards highly expressive and manageable code.
The Power of FromRequest
Before diving into creating our own extractors, let's establish a clear understanding of some core concepts.
- Extractor: In the context of web frameworks, an extractor is a type that implements a specific trait (like
FromRequest) which allows it to be automatically constructed from the incoming HTTP request. This process happens before your handler function is invoked. - Asynchronous Context: Both Axum and Actix Web are asynchronous frameworks. This means that extracting data from a request often involves
awaiting operations (like reading the request body). Therefore, theFromRequestimplementation will typically be anasyncfunction itself, returning aFuture. - Error Handling: Extractors can fail. For instance, if a required header is missing or a JSON body is malformed. A robust
FromRequestimplementation must define how these extraction failures are handled and what kind of error response is returned to the client.
The FromRequest trait is at the heart of how Axum and Actix Web seamlessly provide structured data to your handler functions. When a request arrives, the framework inspects the types of the arguments in your handler. If an argument's type implements FromRequest, the framework calls its from_request method, passing in the incoming request's context. This method then attempts to construct an instance of the argument type. If successful, the instance is passed to your handler; if it fails, an appropriate error response is returned to the client without your handler ever being called. This mechanism promotes a clear separation of concerns, keeping your handlers focused purely on business logic.
Building a Custom Extractor in Axum
Let's imagine we want to extract a "Client-ID" header from every request and ensure it's a valid UUID. We'll create a custom ClientId extractor.
// Axum Example use axum::{ async_trait, extract::{FromRequestParts, Request}, http::{request::Parts, StatusCode}, response::{IntoResponse, Response}, Json, Router, }; use std::fmt; use uuid::Uuid; // Our custom error type for ClientId extraction failures #[derive(Debug)] struct ClientIdError; impl fmt::Display for ClientIdError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Invalid or missing Client-ID header") } } // Convert our error into an HTTP response impl IntoResponse for ClientIdError { fn into_response(self) -> Response { (StatusCode::BAD_REQUEST, "Missing or invalid Client-ID header").into_response() } } // The custom extractor struct #[derive(Debug)] pub struct ClientId(pub Uuid); // Implement FromRequestParts for our ClientId // Axum's FromRequestParts is used for extracting data from request headers, URI, etc. // FromRequest is used for extracting from the body or other async operations that consume the request. #[async_trait] impl FromRequestParts for ClientId { type Rejection = ClientIdError; async fn from_request_parts(parts: &mut Parts, _state: &()) -> Result<Self, Self::Rejection> { let headers = &parts.headers; let client_id_header = headers .get("Client-ID") .ok_or(ClientIdError)? // Error if header is missing .to_str() .map_err(|_| ClientIdError)?; // Error if header is not valid UTF-8 let client_id = Uuid::parse_str(client_id_header).map_err(|_| ClientIdError)?; // Error if not a valid UUID Ok(ClientId(client_id)) } } // An example handler using our custom extractor async fn handle_request_with_client_id(ClientId(client_id): ClientId) -> Json<String> { Json(format!("Hello from client: {}", client_id)) } #[tokio::main] async fn main() { let app = Router::new().route("/hello", axum::routing::get(handle_request_with_client_id)); println!("Axum server running on http://127.0.0.1:3000"); axum::Server::bind(&"0.0.0.1:3000".parse().unwrap()) .serve(app.into_make_service()) .await .unwrap(); }
In the Axum example, we implement FromRequestParts because we are only looking at the request's parts (headers) and not consuming the request body. If we were to parse a JSON body, we would implement FromRequest instead. Notice how ClientIdError implements IntoResponse, allowing Axum to automatically convert our extraction failure into a proper HTTP response.
Building a Custom Extractor in Actix Web
Now, let's implement a similar ClientId extractor for Actix Web. Actix Web's FromRequest trait directly handles both headers and potentially asynchronous operations.
// Actix Web Example use actix_web::{ dev::Payload, error::ResponseError, http::{header, StatusCode}, web, App, FromRequest, HttpRequest, HttpResponse, HttpServer, }; use futures::future::{ok, Ready}; use std::{fmt, ops::Deref}; use uuid::Uuid; // Our custom error type for ClientId extraction failures #[derive(Debug)] struct ClientIdError; impl fmt::Display for ClientIdError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Invalid or missing Client-ID header") } } // Convert our error into an HTTP response impl ResponseError for ClientIdError { fn error_response(&self) -> HttpResponse { HttpResponse::build(self.status_code()) .body(self.to_string()) } fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } } // The custom extractor struct #[derive(Debug)] pub struct ClientId(pub Uuid); // Implement Deref to easily access the inner Uuid impl Deref for ClientId { type Target = Uuid; fn deref(&self) -> &Self::Target { &self.0 } } // Implement FromRequest for our ClientId // Actix Web's FromRequest works for both header and body extraction. impl FromRequest for ClientId { type Error = ClientIdError; type Future = Ready<Result<Self, Self::Error>>; type Config = (); fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { let client_id_header = req.headers().get("Client-ID"); let result = match client_id_header { Some(header_value) => { header_value .to_str() .ok() .and_then(|s| Uuid::parse_str(s).ok()) .map(|uuid| ClientId(uuid)) .ok_or(ClientIdError) } None => Err(ClientIdError), }; ok(result) } } // An example handler using our custom extractor async fn handle_request_with_client_id(client_id: ClientId) -> HttpResponse { HttpResponse::Ok().body(format!("Hello from client: {}", client_id.0)) } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new().service(web::resource("/hello").to(handle_request_with_client_id)) }) .bind("127.0.0.1:8080")? .run() .await }
In the Actix Web example, FromRequest's from_request method receives a HttpRequest reference and a mutable Payload. Because our ClientId extractor only needs header information, we don't need to interact with the Payload. The Deref implementation for ClientId is a nice touch, allowing you to use client_id.to_string() directly instead of client_id.0.to_string().
Application Scenarios and Best Practices
Custom extractors are incredibly versatile and can be applied in numerous scenarios:
- Authentication and Authorization: Extracting JWTs, API keys, or session tokens and validating them.
- Tenant ID Extraction: In multi-tenant applications, extracting a tenant ID from a header, sub-domain, or path.
- Complex Query Parameter Parsing: When query parameters require specific validation or transformation that goes beyond what built-in extractors offer.
- Body Pre-processing: Decrypting or decompressing request bodies before they reach the main handler.
- User Context: Loading a
Userstruct from a database based on an authenticated ID and injecting it into the handler.
When designing custom extractors, consider these best practices:
- Clear Error Handling: Always define a specific error type for your extractor and implement the framework's error conversion trait (
IntoResponsefor Axum,ResponseErrorfor Actix Web). This ensures clear and actionable error messages for clients. - Modularity: Keep extractors focused on a single responsibility. If an extractor becomes too complex, consider breaking it down into smaller, composable units.
- Performance: Be mindful of performance-critical operations within your extractor, especially if they involve disk I/O or network requests. Cache results where appropriate.
- Testability: Design your extractors to be easily testable in isolation. Avoid relying too heavily on complex framework internals that are hard to mock.
- Documentation: Clearly document what your extractor expects (e.g., required headers, body format) and what it provides.
Enhancing Web Development Ergonomics
Custom extractors built upon the FromRequest trait in Axum and Actix Web are a powerful feature that significantly improves the ergonomics, modularity, and testability of your Rust web applications. By encapsulating request data extraction and validation logic, they allow handler functions to remain clean, concise, and focused purely on business logic, leading to more maintainable and robust codebases.

