jonnifyelixir
F6 / F6.01 / controllers

F6.01 · part 3 of 3

Controllers, views & the facade seam

This is where your code lives in the lifecycle, and it is the place the whole F5/F6 split pays off. A controller action receives the matched request, calls the Portal facade, branches on the closed %Portal.Error{} contract, and hands a view its data; the view turns that data into markup. The rule is one sentence: a controller names only Portal and its error struct — never Portal.Engine, never a repo, never a GenServer.call. Keep that rule and the controller stays a thin translation between HTTP and the engine, which is exactly what let the F5.09 lab promise that F6 would change nothing below the facade.

See the three players and the one-way seam between them, then read a real controller and the view it renders.

Three players

The controller, the view, and the facade each do one job. The controller decides and delegates; the facade holds the domain; the view renders. Select one to see its role — and notice that only the controller touches the facade, and only the view touches markup.

The seam · select a player

CONTROLLER calls Portal.*, branches on %Portal.Error{}, renders a view your code VIEW / HEEx renders assigns to markup — calls no engine F6.05 PORTAL FACADE enroll/2, courses_of/1, progress_of/1 — the engine’s edge F5.08

player: Controller

does: calls the facade, picks a view

A fat controller is the usual way a web app rots: domain logic leaks into actions until the engine and the web are tangled. The facade rule keeps the controller honest — if a line is not a facade call, a branch on the result, or a render, it does not belong here.

The seam, one direction

The relationships only point one way. The controller calls down into the facade and hands data sideways to the view; neither the facade nor the view ever calls back into the controller, and the view never calls the facade. That one-way shape is what makes the web layer replaceable — swap a controller for a LiveView in F6.06 and the facade below does not change.

Controller in the middle, calls down, renders sideways

CONTROLLER ACTION decide & delegate calls assigns PORTAL FACADE · F5 dispatch / query VIEW / HEEx · F6.05 renders assigns

One-way dependencies are the whole game. The controller knows the facade and the view; neither knows the controller. Replace the caller and everything it called stays exactly as it was.

A controller and its view

A write action shows the shape: build nothing, call Portal.enroll/2, and map its two outcomes onto a redirect with a flash message. The view is a separate module that embeds templates; the HEEx renders only from assigns, so it has no way to reach the engine even if it wanted to.

# the controller: call the facade, branch on the contract, redirect
defmodule PortalWeb.EnrollmentController do
  use PortalWeb, :controller

  def create(conn, %{"user" => uid, "course" => cid}) do
    case Portal.enroll(uid, cid) do
      :ok ->
        conn |> put_flash(:info, "Enrolled") |> redirect(to: ~p"/courses/#{cid}")

      {:error, %Portal.Error{message: msg}} ->
        conn |> put_flash(:error, msg) |> redirect(to: ~p"/courses")
    end
  end
end

The view module embeds its templates; the HEEx draws from assigns and nothing else:

# the view module: embed the templates for this controller
defmodule PortalWeb.CourseHTML do
  use PortalWeb, :html
  embed_templates "course_html/*"
end

# course_html/show.html.heex — renders only from assigns, calls no engine
<h1>{@course.title}</h1>
<progress value={@progress} max="100"></progress>
<.link navigate={~p"/courses"}>All courses</.link>

decide & delegate

The controller calls the facade and maps the result — the only domain-aware step.

render assigns

The view turns assigns into HEEx — data in, markup out.

That completes F6.01. The next module, F6.02 — Routing, controllers & plugs, builds out the match in the middle of the lifecycle. Back to the module overview or the F6 chapter.

References

Primary sources for this lesson, and where it connects in the course.