Phoenix Controllers And JSON

Copy Markdown View Source

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
end

JSON 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
end

Keep 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.