rust 71 lines · 8 steps

Building a custom Axum extractor for query filters

A custom FromRequestParts implementation parses `field=op.value` query params into a typed list of filters.

Explained by highlit
1use axum::{
2 extract::{FromRequestParts, Query},
3 http::{request::Parts, StatusCode},
4};
5use serde::Deserialize;
6use std::collections::HashMap;
7 
8#[derive(Debug, Clone)]
9pub enum Op {
10 Eq,
11 Neq,
12 Gt,
13 Lt,
14 Like,
15}
16 
17impl Op {
18 fn parse(raw: &str) -> Option<Self> {
19 match raw {
20 "eq" => Some(Op::Eq),
21 "neq" => Some(Op::Neq),
22 "gt" => Some(Op::Gt),
23 "lt" => Some(Op::Lt),
24 "like" => Some(Op::Like),
25 _ => None,
26 }
27 }
28}
29 
30#[derive(Debug, Clone)]
31pub struct Filter {
32 pub field: String,
33 pub op: Op,
34 pub value: String,
35}
36 
37#[derive(Debug, Default)]
38pub struct Filters(pub Vec<Filter>);
39 
40impl<S> FromRequestParts<S> for Filters
41where
42 S: Send + Sync,
43{
44 type Rejection = (StatusCode, String);
45 
46 async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
47 let Query(raw): Query<HashMap<String, String>> =
48 Query::from_request_parts(parts, state)
49 .await
50 .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
51 
52 let mut filters = Vec::with_capacity(raw.len());
53 for (field, spec) in raw {
54 let (op_raw, value) = spec.split_once('.').ok_or_else(|| {
55 (
56 StatusCode::BAD_REQUEST,
57 format!("filter `{field}` must be `<op>.<value>`"),
58 )
59 })?;
60 let op = Op::parse(op_raw).ok_or_else(|| {
61 (StatusCode::BAD_REQUEST, format!("unknown operator `{op_raw}`"))
62 })?;
63 filters.push(Filter {
64 field,
65 op,
66 value: value.to_owned(),
67 });
68 }
69 Ok(Filters(filters))
70 }
71}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Implementing FromRequestParts lets a handler receive validated domain types instead of raw query maps.
  2. 2Composing on top of Axum's built-in Query extractor reuses its parsing while adding your own semantics.
  3. 3Returning a (StatusCode, String) rejection turns every parse failure into a clean HTTP 400 response.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Building a custom Axum extractor for query filters — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code