#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:

  1. See every parameter name in from_bus() โ€” no naming conventions to memorize
  2. Control defaults per-parameter โ€” query_param_or("page", 1) vs query_param("status")
  3. Add validation logic inline โ€” if per_page > 200 { ... }
  4. Test from_bus() directly โ€” construct a Bus with QueryParams::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.


  • `PageParams` API โ€” Built-in pagination types
  • Outcome Patterns Cookbook โ€” Outcome composition
  • JSON Outcomes Cookbook โ€” get_json_out / post_typed_json_out