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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Implementing FromRequestParts lets a handler receive validated domain types instead of raw query maps.
- 2Composing on top of Axum's built-in Query extractor reuses its parsing while adding your own semantics.
- 3Returning a (StatusCode, String) rejection turns every parse failure into a clean HTTP 400 response.
Related explainers
javascript
const express = require('express'); const { createProxyMiddleware, fixRequestBody } = require('http-proxy-middleware'); const router = express.Router();
Building an API gateway proxy in Express
reverse-proxy
middleware
api-gateway
Intermediate
6 steps
python
import asyncio from dataclasses import dataclass import aiohttp
Bounded-concurrency HTTP fetching with asyncio
async
concurrency
semaphore
Intermediate
8 steps
rust
use std::sync::Arc; use axum::{ extract::{Multipart, State}, http::StatusCode,
Throttling file uploads in Axum with a Semaphore
concurrency
backpressure
multipart
Advanced
8 steps
rust
use once_cell::sync::Lazy; use regex::Regex; static NON_ALPHANUMERIC: Lazy<Regex> = Lazy::new(|| Regex::new(r"[^a-z0-9]+").unwrap());
Building URL slugs in Rust
string-processing
regex
transliteration
Intermediate
8 steps
go
package api import ( "errors"
Turning Gin validation errors into JSON
validation
error-handling
http-handlers
Intermediate
9 steps
python
from flask import Blueprint, jsonify from sqlalchemy import text from sqlalchemy.exc import SQLAlchemyError
Building a health check endpoint in Flask
health-check
blueprint
error-handling
Intermediate
6 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-custom-axum-extractor-for-query-filters-explained-rust-de26/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.