This guide is the canonical v1.21 request-edge story: browser params enter at the web edge, Scrypath.QueryParams normalizes them into plain data, optional Scrypath.Phoenix helpers round-trip params and attempted values, your context calls Scrypath.search/3, and the runtime stops there.
If you want the broader onboarding path first, read Getting Started or the Golden path. If you want reusable search defaults, metadata-backed host rendering, or multi-search composition after this shared contract, continue with Composing real-app search.
The Boundary
Keep the lane narrow and explicit:
- Browser params arrive in a controller, LiveView, or another app-owned web edge.
Scrypath.QueryParams.normalize/1turns request-shaped params into one stable plain-data contract.Scrypath.Phoenixis optional glue for params, forms, and URL round-tripping.QueryParams.to_search_args/1prepares{query, search_opts}for your context.- Your context calls
Scrypath.search/3.
Scrypath.Phoenix does not execute search, own socket lifecycle, or replace contexts. %Scrypath.Query{} is not public API.
Framework-light core
Scrypath.QueryParams is the framework-light public edge seam:
case Scrypath.QueryParams.normalize(params) do
{:ok, query_params} ->
{query, search_opts} = Scrypath.QueryParams.to_search_args(query_params)
MyApp.Content.search_posts(query, search_opts)
{:error, error_map} ->
{:error, error_map}
endThat shape works outside Phoenix too. The normalized output is plain data that feeds the same context-owned runtime path.
Optional Phoenix glue
If you are in Phoenix, Scrypath.Phoenix removes repeated request-edge glue without becoming a second runtime:
alias Scrypath.Phoenix, as: SearchPhoenix
alias Scrypath.QueryParams
case SearchPhoenix.from_params(params) do
{:ok, query_params} ->
form = SearchPhoenix.to_form_data(query_params)
{query, search_opts} = QueryParams.to_search_args(query_params)
{:ok, result} = MyApp.Content.search_posts(query, search_opts)
{:ok, %{form: form, result: result}}
{:error, error_map} ->
{:error, SearchPhoenix.to_form_data(params, error_map)}
endUse that helper layer for:
- browser-shaped param normalization
- renderable attempted values plus field/form errors
- URL param round-tripping
Do not use it for:
- search execution
- repo access
- controller macros or
use Scrypath.Phoenix - LiveView socket ownership
Contexts stay canonical
Contexts remain the application boundary for search orchestration:
defmodule MyApp.Content do
alias MyApp.Blog.Post
alias MyApp.Repo
def search_posts(query, opts \\ []) do
Scrypath.search(Post, query,
Keyword.merge([backend: Scrypath.Meilisearch, repo: Repo], opts)
)
end
endThat is where repo-backed hydration, backend choice, preload policy, sync mode choice, and feature-level defaults belong.
Controller and LiveView flow
Controllers and LiveView stay thin:
- controllers normalize params, call the context, and render HTML or JSON
handle_params/3remains the canonical LiveView source of truth for URL-driven search state- the same
QueryParams/SearchPhoenixcontract feeds both
See:
Example app vs guides
HexDocs is the teaching surface for this public contract. The runnable example app is the proof/runbook surface for the real Postgres + Meilisearch + Oban path, CI parity, and local smoke commands:
- teaching surface: this guide plus the rest of
guides/ - proof/runbook surface:
examples/phoenix_meilisearch/README.md
Use the guide to understand the boundary. Use the example when you want to prove the operational path against real services.