#Explicit Search Params Cookbook
Version: 0.44.0 | Updated: 2026-03-29 | Applies to: ranvier-http 0.44+ | Category: Cookbook
#Overview
Ranvier uses explicit search parameter structs instead of derive macros for query extraction. Each parameter's name, type, and default is visible in code โ aligned with P4 (Bus = explicit type-indexed access) and P5 (No convention-over-configuration).
Key types: PageParams, Paginated<T> (from ranvier_http::prelude)
#1. `PageParams` โ Built-in Pagination
PageParams extracts page and per_page query parameters with automatic clamping:
use ranvier_core::prelude::*;
use ranvier_http::prelude::*;
#[transition]
async fn list_items(_: (), _: &(), bus: &mut Bus) -> Outcome<Paginated<Item>, String> {
let page = PageParams::from_bus(bus);
// page.page = 1 (default), page.per_page = 20 (default)
// With ?page=3&per_page=50 โ page.page = 3, page.per_page = 50
let pool = try_outcome!(bus.get_cloned::<PgPool>(), "PgPool");
let total: i64 = try_outcome!(
sqlx::query_scalar("SELECT COUNT(*) FROM items")
.fetch_one(&*pool).await, "count"
);
let items: Vec<Item> = try_outcome!(
sqlx::query_as("SELECT * FROM items ORDER BY id LIMIT $1 OFFSET $2")
.bind(page.per_page)
.bind(page.offset())
.fetch_all(&*pool).await, "query"
);
Outcome::Next(Paginated::new(items, total, &page))
}Response:
{
"items": [...],
"total_count": 142,
"page": 3,
"per_page": 50,
"total_pages": 3
}#2. Custom Search Struct โ The Explicit Pattern
For domain-specific filters, create a plain struct with a from_bus method:
use ranvier_http::prelude::*;
struct InterfaceSearch {
page: PageParams,
intf_id: Option<String>,
intf_nm: Option<String>,
status: Option<String>,
use_yn: Option<String>,
}
impl InterfaceSearch {
fn from_bus(bus: &Bus) -> Self {
Self {
page: PageParams::from_bus(bus),
intf_id: bus.query_param("intf_id"),
intf_nm: bus.query_param("intf_nm"),
status: bus.query_param("status"),
use_yn: bus.query_param("use_yn"),
}
}
}Usage in a transition:
use crate::safe_query_builder::QueryBuilder;
#[transition]
async fn search_interfaces(_: (), _: &(), bus: &mut Bus) -> Outcome<Paginated<InterfaceInfo>, String> {
let search = InterfaceSearch::from_bus(bus);
let pool = try_outcome!(bus.get_cloned::<PgPool>(), "PgPool");
// Copy the helper from db-sqlx-demo/src/safe_query_builder.rs
let query = QueryBuilder::new("SELECT * FROM tb_intf_inf")
.filter_optional("intf_id", &search.intf_id)
.filter_optional("intf_nm", &search.intf_nm)
.filter_optional("use_yn", &search.use_yn)
.paginate(search.page.per_page, search.page.offset())
.build();
// Execute and return...
Outcome::Next(Paginated::new(items, total, &search.page))
}#3. Why Not `#[derive(QueryParams)]`?
A derive macro would auto-extract fields by name:
// โ NOT supported โ violates P4/P5
#[derive(QueryParams)]
struct Search { intf_id: Option<String>, page: i64 }This approach hides which query parameters are read and what defaults apply. With the explicit pattern, you can:
- See every parameter name in
from_bus()โ no naming conventions to memorize - Control defaults per-parameter โ
query_param_or("page", 1)vsquery_param("status") - Add validation logic inline โ
if per_page > 200 { ... } - Test
from_bus()directly โ construct aBuswithQueryParams::from_query("...")in tests
The cost is ~10 lines per search struct. For EIMS (3 search endpoints), that's ~30 lines โ a reasonable price for full visibility.
#Related
- `PageParams` API โ Built-in pagination types
- Outcome Patterns Cookbook โ Outcome composition
- JSON Outcomes Cookbook โ
get_json_out/post_typed_json_out