Phoenix

Phoenix is Elixir's flagship web framework: fast, productive, real-time.

45.1 Getting started

mix archive.install hex phx_new
mix phx.new blog            # --no-ecto, --database sqlite3, --no-html, --live, --binary-id
cd blog
mix ecto.create
mix phx.server              # or iex -S mix phx.server  → http://localhost:4000

45.2 Project structure

lib/
├── blog/                   # business logic ("contexts"), Ecto schemas, Repo
│   ├── application.ex
│   ├── repo.ex
│   └── posts.ex            # context module
└── blog_web/               # web interface
    ├── endpoint.ex
    ├── router.ex
    ├── controllers/
    ├── components/         # function components, layouts, core_components.ex
    └── live/               # LiveViews
priv/repo/migrations/
assets/                     # JS, CSS (esbuild + tailwind)
config/
test/

45.3 Request lifecycle

Endpoint (plugs: static, logger, parsers, session) → Router (pipelines + routes) → Controller action → View/Component → response.

45.4 Plug — the composable HTTP abstraction

Plug.Conn functions: assign/3, put_status/2, put_resp_header/3, send_resp/3, halt/1, fetch_session/2, get_session/2, put_session/3.

defmodule BlogWeb.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, "Login required") |> redirect(to: "/login") |> halt()
    end
  end
end

45.5 Router

mix phx.routes lists routes. Verified routes: ~p"/posts/#{post}" are checked at compile time.

defmodule BlogWeb.Router do
  use BlogWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {BlogWeb.Layouts, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api, do: plug(:accepts, ["json"])

  scope "/", BlogWeb do
    pipe_through :browser
    get "/", PageController, :home
    resources "/posts", PostController       # index/show/new/create/edit/update/delete
    live "/dashboard", DashboardLive
  end

  scope "/api", BlogWeb.API do
    pipe_through :api
    resources "/posts", PostController, only: [:index, :show]
  end
end

45.6 Controllers

defmodule BlogWeb.PostController do
  use BlogWeb, :controller
  alias Blog.Posts

  def index(conn, _params), do: render(conn, :index, posts: Posts.list_posts())

  def create(conn, %{"post" => params}) do
    case Posts.create_post(params) do
      {:ok, post} ->
        conn |> put_flash(:info, "Created!") |> redirect(to: ~p"/posts/#{post}")
      {:error, changeset} ->
        render(conn, :new, changeset: changeset)
    end
  end
end

# JSON API
def show(conn, %{"id" => id}), do: json(conn, %{data: Posts.get_post!(id)})

45.7 Templates & components (HEEx)

HEEx validates HTML at compile time; {@x} (or <%= @x %>) interpolates with automatic escaping. Slots: slot :inner_block, render_slot(@inner_block).

defmodule BlogWeb.PostHTML do
  use BlogWeb, :html
  embed_templates "post_html/*"

  attr :post, :map, required: true
  def post_card(assigns) do
    ~H"""
    <div class="card">
      <h2>{@post.title}</h2>
      <p :if={@post.published}>Published</p>
      <ul><li :for={tag <- @post.tags}>{tag}</li></ul>
      <.link navigate={~p"/posts/#{@post}"}>Read more</.link>
    </div>
    """
  end
end

45.8 Contexts & generators

Contexts group related functionality (Accounts, Catalog), hiding Ecto details from the web layer.

mix phx.gen.html Posts Post posts title:string body:text published:boolean
mix phx.gen.json Accounts User users email:string:unique
mix phx.gen.live Catalog Product products name:string price:decimal
mix phx.gen.context / phx.gen.schema
mix phx.gen.auth Accounts User users        # complete authentication system

45.9 Channels & PubSub

Phoenix.Presence tracks who is online across a cluster (CRDT-based).

# Server
defmodule BlogWeb.RoomChannel do
  use Phoenix.Channel
  def join("room:" <> _id, _params, socket), do: {:ok, socket}
  def handle_in("new_msg", %{"body" => body}, socket) do
    broadcast!(socket, "new_msg", %{body: body})
    {:noreply, socket}
  end
end

# Anywhere
Phoenix.PubSub.subscribe(Blog.PubSub, "posts")
Phoenix.PubSub.broadcast(Blog.PubSub, "posts", {:post_created, post})

45.10 Testing Phoenix

DataCase wraps each test in a sandboxed DB transaction (Ecto.Adapters.SQL.Sandbox) — tests run concurrently.

defmodule BlogWeb.PostControllerTest do
  use BlogWeb.ConnCase
  test "GET /posts", %{conn: conn} do
    conn = get(conn, ~p"/posts")
    assert html_response(conn, 200) =~ "Listing Posts"
  end
end

45.11 Security features

CSRF protection, secure headers, HTML escaping, parameterized SQL via Ecto, Plug.Crypto, password hashing (bcrypt_elixir, argon2_elixir), Sobelow for audits.