Request Extractors
Type-safe extraction of data from HTTP requests using extractors and helper macros.
Table of Contents
- Overview
- Features
- Installation
- Extractor Types
- Helper Macros
- Traits
- Best Practices
- API Reference
- Summary
Overview
The armature-core extractors module provides a type-safe way to extract data from HTTP requests, similar to NestJS decorators or Axum extractors. Instead of manually parsing request bodies, query parameters, and headers, you can use strongly-typed extractors that handle deserialization and error handling automatically.
Features
- โ Type-safe body extraction with JSON deserialization
- โ Query parameter extraction with struct mapping
- โ Path parameter extraction with type coercion
- โ Header extraction with optional values
- โ Form data extraction
- โ Raw body access
- โ Content-Type and Method extractors
- โ Convenient helper macros for concise syntax
Installation
Request extractors are included in armature-core:
[dependencies]
armature-framework = { version = "0.1", features = ["core"] }
Or use directly:
[dependencies]
armature-core = "0.1"
Extractor Types
Body
Extracts and deserializes the request body as JSON.
use armature_core::extractors::{Body, FromRequest};
use serde::Deserialize;
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
age: Option<u32>,
}
fn create_user_handler(request: &HttpRequest) -> Result<(), Error> {
let body: Body<CreateUser> = Body::from_request(request)?;
println!("Creating user: {}", body.name);
println!("Email: {}", body.email);
// Access inner value
let user = body.into_inner();
Ok(())
}
Query
Extracts and deserializes query parameters into a struct.
use armature_core::extractors::{Query, FromRequest};
use serde::Deserialize;
#[derive(Deserialize)]
struct Pagination {
page: Option<u32>,
limit: Option<u32>,
sort: Option<String>,
}
fn list_users_handler(request: &HttpRequest) -> Result<(), Error> {
// Request: GET /users?page=2&limit=20&sort=name
let query: Query<Pagination> = Query::from_request(request)?;
let page = query.page.unwrap_or(1);
let limit = query.limit.unwrap_or(10);
println!("Page: {}, Limit: {}", page, limit);
Ok(())
}
Path
Extracts a single path parameter by name and parses it to the specified type.
use armature_core::extractors::{Path, FromRequestNamed};
fn get_user_handler(request: &HttpRequest) -> Result<(), Error> {
// Route: /users/:id
let id: Path<u32> = Path::from_request(request, "id")?;
println!("Fetching user with ID: {}", *id);
// Convert to inner value
let user_id: u32 = id.into_inner();
Ok(())
}
PathParams
Extracts all path parameters into a struct.
use armature_core::extractors::{PathParams, FromRequest};
use serde::Deserialize;
#[derive(Deserialize)]
struct UserPostParams {
user_id: u32,
post_id: u32,
}
fn get_user_post_handler(request: &HttpRequest) -> Result<(), Error> {
// Route: /users/:user_id/posts/:post_id
let params: PathParams<UserPostParams> = PathParams::from_request(request)?;
println!("User: {}, Post: {}", params.user_id, params.post_id);
Ok(())
}
Header
Extracts a single header value by name.
use armature_core::extractors::{Header, FromRequestNamed};
fn auth_handler(request: &HttpRequest) -> Result<(), Error> {
let auth: Header = Header::from_request(request, "Authorization")?;
println!("Auth header: {}", auth.value());
// Get as owned String
let token: String = auth.into_value();
Ok(())
}
Headers
Extracts all headers as a HashMap.
use armature_core::extractors::{Headers, FromRequest};
fn debug_handler(request: &HttpRequest) -> Result<(), Error> {
let headers: Headers = Headers::from_request(request)?;
for (name, value) in headers.iter() {
println!("{}: {}", name, value);
}
// Check for specific header
if let Some(content_type) = headers.get("Content-Type") {
println!("Content-Type: {}", content_type);
}
Ok(())
}
RawBody
Extracts the raw request body as bytes.
use armature_core::extractors::{RawBody, FromRequest};
fn raw_handler(request: &HttpRequest) -> Result<(), Error> {
let raw: RawBody = RawBody::from_request(request)?;
println!("Body length: {} bytes", raw.len());
// Get as Vec<u8>
let bytes: Vec<u8> = raw.into_inner();
// Or as string (if valid UTF-8)
let text = raw.as_str()?;
Ok(())
}
Form
Extracts and deserializes form data (application/x-www-form-urlencoded).
use armature_core::extractors::{Form, FromRequest};
use serde::Deserialize;
#[derive(Deserialize)]
struct LoginForm {
username: String,
password: String,
remember_me: Option<bool>,
}
fn login_handler(request: &HttpRequest) -> Result<(), Error> {
let form: Form<LoginForm> = Form::from_request(request)?;
println!("Username: {}", form.username);
Ok(())
}
ContentType
Extracts the Content-Type header.
use armature_core::extractors::{ContentType, FromRequest};
fn content_handler(request: &HttpRequest) -> Result<(), Error> {
let content_type: ContentType = ContentType::from_request(request)?;
if content_type.is_json() {
// Handle JSON
} else if content_type.is_form() {
// Handle form data
}
println!("Content-Type: {}", content_type.value());
Ok(())
}
Method
Extracts the HTTP method.
use armature_core::extractors::{Method, FromRequest};
fn method_handler(request: &HttpRequest) -> Result<(), Error> {
let method: Method = Method::from_request(request)?;
match method.as_str() {
"GET" => println!("Handling GET"),
"POST" => println!("Handling POST"),
_ => println!("Other method: {}", method.as_str()),
}
Ok(())
}
Helper Macros
For more concise syntax, use the helper macros that extract and unwrap in one step.
body!
Extract and deserialize request body.
use armature_framework::prelude::*;
fn handler(request: &HttpRequest) -> Result<(), Error> {
// Extract body as CreateUser
let user = body!(request, CreateUser)?;
println!("Name: {}", user.name);
Ok(())
}
query!
Extract and deserialize query parameters.
use armature_framework::prelude::*;
fn handler(request: &HttpRequest) -> Result<(), Error> {
// Extract query params as Pagination
let pagination = query!(request, Pagination)?;
println!("Page: {}", pagination.page.unwrap_or(1));
Ok(())
}
path!
Extract a path parameter by name with type conversion.
use armature_framework::prelude::*;
fn handler(request: &HttpRequest) -> Result<(), Error> {
// Extract "id" parameter as u32
let id: u32 = path!(request, "id", u32)?;
// Extract "slug" parameter as String
let slug: String = path!(request, "slug", String)?;
println!("ID: {}, Slug: {}", id, slug);
Ok(())
}
header!
Extract a header value by name.
use armature_framework::prelude::*;
fn handler(request: &HttpRequest) -> Result<(), Error> {
// Extract Authorization header
let auth: String = header!(request, "Authorization")?;
// Extract custom header
let request_id: String = header!(request, "X-Request-ID")?;
println!("Auth: {}", auth);
Ok(())
}
Traits
FromRequest
Trait for extractors that don't need a parameter name.
pub trait FromRequest: Sized {
fn from_request(request: &HttpRequest) -> Result<Self, Error>;
}
Implemented by: Body<T>, Query<T>, PathParams<T>, Headers, RawBody, Form<T>, ContentType, Method
FromRequestNamed
Trait for extractors that require a parameter name.
pub trait FromRequestNamed: Sized {
fn from_request(request: &HttpRequest, name: &str) -> Result<Self, Error>;
}
Implemented by: Path<T>, Header
Best Practices
1. Use Specific Types
// โ
Good - specific types
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
let body: Body<CreateUser> = Body::from_request(&request)?;
// โ Bad - generic JSON value
let body: Body<serde_json::Value> = Body::from_request(&request)?;
2. Make Fields Optional When Appropriate
#[derive(Deserialize)]
struct SearchParams {
query: String, // Required
page: Option<u32>, // Optional with default
limit: Option<u32>, // Optional with default
sort: Option<String>, // Optional
}
let params = query!(request, SearchParams)?;
let page = params.page.unwrap_or(1);
let limit = params.limit.unwrap_or(20).min(100); // Cap at 100
3. Handle Extraction Errors Gracefully
fn handler(request: &HttpRequest) -> HttpResponse {
// Using match for custom error handling
let body = match body!(request, CreateUser) {
Ok(user) => user,
Err(e) => {
return HttpResponse::bad_request()
.json(json!({ "error": format!("Invalid body: {}", e) }));
}
};
// Process body...
HttpResponse::ok()
}
4. Use Macros for Concise Code
// โ
Concise with macros
fn handler(request: &HttpRequest) -> Result<HttpResponse, Error> {
let user = body!(request, CreateUser)?;
let filters = query!(request, Filters)?;
let id: u32 = path!(request, "id", u32)?;
let auth = header!(request, "Authorization")?;
// Handle request...
Ok(HttpResponse::ok())
}
// โ Verbose without macros
fn handler(request: &HttpRequest) -> Result<HttpResponse, Error> {
let user = Body::<CreateUser>::from_request(request)?.into_inner();
let filters = Query::<Filters>::from_request(request)?.into_inner();
let id = Path::<u32>::from_request(request, "id")?.into_inner();
let auth = Header::from_request(request, "Authorization")?.into_value();
// Handle request...
Ok(HttpResponse::ok())
}
5. Validate After Extraction
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
age: u32,
}
fn handler(request: &HttpRequest) -> Result<HttpResponse, Error> {
let user = body!(request, CreateUser)?;
// Validate after extraction
if user.name.is_empty() {
return Err(Error::Validation("Name cannot be empty".into()));
}
if !user.email.contains('@') {
return Err(Error::Validation("Invalid email format".into()));
}
if user.age < 18 {
return Err(Error::Validation("Must be 18 or older".into()));
}
// Process valid user...
Ok(HttpResponse::created())
}
Common Pitfalls
- โ Don't forget to add
#[derive(Deserialize)]on extraction structs - โ Don't use
Stringwhen a more specific type works (useu32for IDs) - โ Don't ignore extraction errors in production code
- โ
Do use
Option<T>for optional parameters - โ Do use the macros for cleaner code
- โ Do validate data after extraction
API Reference
Extractor Types
| Type | Trait | Description |
|---|---|---|
Body<T> |
FromRequest |
JSON body deserialization |
Query<T> |
FromRequest |
Query parameter deserialization |
Path<T> |
FromRequestNamed |
Single path parameter extraction |
PathParams<T> |
FromRequest |
All path parameters deserialization |
Header |
FromRequestNamed |
Single header extraction |
Headers |
FromRequest |
All headers extraction |
RawBody |
FromRequest |
Raw body bytes |
Form<T> |
FromRequest |
Form data deserialization |
ContentType |
FromRequest |
Content-Type header |
Method |
FromRequest |
HTTP method |
Macros
| Macro | Syntax | Returns |
|---|---|---|
body! |
body!(request, Type) |
Result<Type, Error> |
query! |
query!(request, Type) |
Result<Type, Error> |
path! |
path!(request, "name", Type) |
Result<Type, Error> |
header! |
header!(request, "Name") |
Result<String, Error> |
Summary
Key Points:
- Use extractors for type-safe request data extraction
- Use macros (
body!,query!,path!,header!) for concise syntax - Implement
FromRequestfor extractors without names - Implement
FromRequestNamedfor named parameter extraction - Always validate data after extraction
- Use
Option<T>for optional fields
Quick Reference:
use armature_framework::prelude::*;
fn handler(request: &HttpRequest) -> Result<HttpResponse, Error> {
// Body extraction
let user = body!(request, CreateUser)?;
// Query extraction
let filters = query!(request, Filters)?;
// Path extraction
let id: u32 = path!(request, "id", u32)?;
// Header extraction
let auth = header!(request, "Authorization")?;
Ok(HttpResponse::ok())
}