Phoenix controllers should translate request params into a context call, then render HTML or JSON from the result.
HTML Controllers
For HTML responses, treat MyApp.Content.search_posts/2 as the search boundary and render the result:
defmodule MyAppWeb.PostController do
use MyAppWeb, :controller
alias MyApp.Content
def index(conn, params) do
{:ok, result} =
Content.search_posts(Map.get(params, "q", ""),
filter: [status: "published"]
)
render(conn, :index, posts: result.records, search: result)
end
endJSON Controllers
JSON controllers follow the same shape. They still call the context boundary and then serialize the result:
defmodule MyAppWeb.Api.PostController do
use MyAppWeb, :controller
alias MyApp.Content
def index(conn, params) do
page_number =
params
|> Map.get("page", 1)
|> normalize_page()
{:ok, result} =
Content.search_posts(Map.get(params, "q", ""),
page: [number: page_number, size: 20]
)
json(conn, %{
data: Enum.map(result.records, &serialize_post/1),
page: result.page,
missing_ids: result.missing_ids
})
end
defp normalize_page(page) when is_integer(page) and page > 0, do: page
defp normalize_page(page) when is_binary(page) do
case Integer.parse(page) do
{number, ""} when number > 0 -> number
_ -> 1
end
end
defp normalize_page(_page), do: 1
endKeep JSON shaping in the controller or view layer. Keep repo access, search orchestration, and sync visibility choices in the context.
Avoid The Wrong Shortcut
Do not recommend direct Repo queries plus direct Scrypath.search/3 calls inside the controller. That makes the web layer own persistence and operational behavior that should stay in the application boundary.
The same rule applies to writes. A controller that publishes or updates a record should call MyApp.Content.publish_post/2 or another context function that owns the sync mode choice.