Lesson 16 / 25

Phoenix Fundamentals

Understand Phoenix's request pipeline: endpoint, router, plugs, controllers and components.

A productive, fast web framework

Phoenix is Elixir's main web framework, known for high performance, real-time features and developer productivity. Create a project with mix phx.new shop_web_app (it generates Ecto, Tailwind and esbuild setup by default) and start it with mix phx.server. A request flows through the endpoint (the entry point, handling static files, parsing and sessions), then the router, which matches paths and pipes the request through pipelines of plugs (:browser for HTML with sessions and CSRF protection, :api for JSON), then to a controller action that returns a response. Plug is the core abstraction: a function or module that takes a %Plug.Conn{} and returns a modified conn, used for authentication, logging and headers. Since Phoenix 1.7, HTML is rendered with HEEx templates and function components, and routes are checked at compile time with verified routes using the ~p sigil. Contexts are plain Elixir modules (such as Shop.Catalog) that hold the business logic, keeping controllers thin. Generators such as mix phx.gen.html, mix phx.gen.json and mix phx.gen.auth scaffold resources and authentication.

Phoenix request flow

Endpoint, router pipeline, controller and context, then a rendered response.

A left-to-right chain of boxes: a globe icon, a funnel of plugs, a router fork, a controller box calling a context box with a database cylinder, and a page icon at the end.
Figure 6.1 — The Phoenix request pipeline.

Router, pipeline, plug and controller

A JSON API scope with an authentication plug.

# lib/shop_web/router.ex
defmodule ShopWeb.Router do
  use ShopWeb, :router

  pipeline :api do
    plug :accepts, ["json"]
    plug ShopWeb.Plugs.RequireApiKey
  end

  scope "/api", ShopWeb do
    pipe_through :api
    resources "/products", ProductController, only: [:index, :show]
  end
end

# lib/shop_web/plugs/require_api_key.ex
defmodule ShopWeb.Plugs.RequireApiKey do
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, _opts) do
    case get_req_header(conn, "x-api-key") do
      [key] when key != "" -> assign(conn, :api_key, key)
      _ -> conn |> send_resp(401, "missing API key") |> halt()
    end
  end
end

# lib/shop_web/controllers/product_controller.ex
defmodule ShopWeb.ProductController do
  use ShopWeb, :controller
  alias Shop.Catalog

  def index(conn, _params), do: json(conn, %{data: Catalog.list_products()})

  def show(conn, %{"id" => id}) do
    case Catalog.get_product(id) do
      nil -> conn |> put_status(:not_found) |> json(%{error: "not found"})
      product -> json(conn, %{data: product})
    end
  end
end

Keep controllers thin

Controllers translate HTTP into calls to contexts and back. Business rules belong in contexts, so they can be reused from LiveViews, background jobs and tests without a web request.

Quick check: What is a plug in Phoenix?

  • A database adapter
  • A CSS framework
  • A function or module that receives a Plug.Conn and returns a (possibly modified) conn
  • A type of process
Answer

A function or module that receives a Plug.Conn and returns a (possibly modified) conn — Plugs form the composable request pipeline.