Controllers

Phoenix controllers — actions, params, responses, plugs, and error handling

Controllers handle HTTP requests and return responses. They are Elixir modules with action functions that receive a Plug.Conn struct and params.

Basic controller

defmodule MyAppWeb.PostController do
  use MyAppWeb, :controller

  def index(conn, _params) do
    posts = Blog.list_posts()
    render(conn, :index, posts: posts)
  end

  def show(conn, %{"id" => id}) do
    post = Blog.get_post!(id)
    render(conn, :show, post: post)
  end
end

Actions

Action HTTP verb Purpose
:index GET List all resources
:new GET Show creation form
:create POST Save new resource
:show GET Display single resource
:edit GET Show edit form
:update PATCH/PUT Save changes
:delete DELETE Remove resource

Params

# Path params
def show(conn, %{"id" => id}) do
  post = Blog.get_post!(id)
  render(conn, :show, post: post)
end

# Query params
def index(conn, %{"page" => page}) do
  posts = Blog.list_posts(page: page)
  render(conn, :index, posts: posts)
end

# All params available
def create(conn, params) do
  # params = %{"post" => %{"title" => "...", "body" => "..."}, "_csrf_token" => "..."}
  ...
end

Rendering responses

# Render a template with assigns
render(conn, :index, posts: posts)

# Render with status code
conn
|> put_status(:created)
|> render(:show, post: post)

# JSON response
json(conn, %{id: post.id, title: post.title})

# Plain text
text(conn, "OK")

# HTML directly
html(conn, "<h1>Hello</h1>")

Redirecting

# To a verified route
redirect(conn, to: ~p"/posts")

# External URL
redirect(conn, external: "https://example.com")

# With flash message
conn
|> put_flash(:info, "Post created successfully.")
|> redirect(to: ~p"/posts")

Flash messages

# In controller
conn
|> put_flash(:info, "Operation succeeded.")
|> put_flash(:error, "Something went wrong.")

# In templates (HEEx)
<p class="alert alert-info"><%= flash[:info] %></p>
<p class="alert alert-error"><%= flash[:error] %></p>

Plug pipeline

Plugs are composable middleware that transform the connection:

defmodule MyAppWeb.Plugs.RequireAuth do
  import Plug.Conn
  import Phoenix.Controller

  def init(opts), do: opts

  def call(conn, _opts) do
    if conn.assigns[:current_user] do
      conn
    else
      conn
      |> put_flash(:error, "You must be logged in.")
      |> redirect(to: ~p"/login")
      |> halt()
    end
  end
end

Using plugs in controllers

defmodule MyAppWeb.DashboardController do
  use MyAppWeb, :controller

  plug :require_auth when action in [:index, :show]
  plug :load_post when action in [:show, :edit, :update, :delete]

  def index(conn, _params), do: ...

  defp require_auth(conn, _) do
    if conn.assigns[:current_user] do
      conn
    else
      conn |> redirect(to: ~p"/login") |> halt()
    end
  end

  defp load_post(conn, _) do
    assign(conn, :post, Blog.get_post!(conn.params["id"]))
  end
end

Action fallbacks

Use action_fallback to handle common error patterns:

defmodule MyAppWeb.PostController do
  use MyAppWeb, :controller
  action_fallback MyAppWeb.FallbackController

  def show(conn, %{"id" => id}) do
    case Blog.get_post(id) do
      {:ok, post} -> render(conn, :show, post: post)
      {:error, :not_found} -> {:error, :not_found}
    end
  end
end

defmodule MyAppWeb.FallbackController do
  use MyAppWeb, :controller

  def call(conn, {:error, :not_found}) do
    conn
    |> put_status(:not_found)
    |> put_view(html: MyAppWeb.ErrorHTML, json: MyAppWeb.ErrorJSON)
    |> render("404.html", [])
  end
end

Error pages

Custom error pages are defined in ErrorHTML and ErrorJSON:

# lib/my_app_web/controllers/error_html.ex
defmodule MyAppWeb.ErrorHTML do
  use MyAppWeb, :html

  def render("404.html", _assigns) do
    "Page not found"
  end

  def render("500.html", _assigns) do
    "Internal server error"
  end
end

# lib/my_app_web/controllers/error_json.ex
defmodule MyAppWeb.ErrorJSON do
  def render("404.json", _assigns) do
    %{errors: %{detail: "Not found"}}
  end
end

Conn assigns

# Set assigns
conn = assign(conn, :title, "My Page")
conn = assign(conn, title: "My Page", user: user)

# Get assigns
conn.assigns[:title]
conn.assigns.title

# assign_new — only set if not already present
conn = assign_new(conn, :page_title, fn -> "Default" end)