Elixir Examples
Advanced 35 min read Phase 6

Prerequisites

Phoenix Framework

Build modern web applications with Phoenix, Elixir's premier web framework. Covers routing, controllers, views, Plug middleware, channels, and project structure.

Phoenix is the most popular web framework in the Elixir ecosystem. It leverages the Erlang VM’s ability to handle millions of concurrent connections while maintaining developer productivity with conventions inspired by the best ideas in web development. Phoenix gives you real-time features out of the box, excellent performance, and a delightful developer experience.

Creating a New Phoenix Project

Phoenix ships with a project generator that scaffolds everything you need. Install the generator with:

mix archive.install hex phx_new

Then create a new project:

mix phx.new my_app
cd my_app
mix deps.get
mix ecto.create
mix phx.server

This creates a full project structure with routing, controllers, HTML templates, asset pipelines, database configuration, and tests – all wired together and ready to run.

<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>Tip
If you do not need a database, pass --no-ecto. For an API-only project, pass --no-html --no-assets. Phoenix generators are highly configurable to fit your needs.

Project Structure

A freshly generated Phoenix project has a well-organized directory layout:

my_app/
  lib/
    my_app/              # Business logic (context modules, schemas)
      application.ex     # OTP Application supervision tree
      repo.ex            # Ecto repository
    my_app_web/          # Web-facing code
      components/        # Function components and layouts
      controllers/       # Request handlers
      endpoint.ex        # Entry point for all requests
      router.ex          # Route definitions
      telemetry.ex       # Telemetry events
  priv/
    repo/migrations/     # Database migrations
    static/              # Static assets served directly
  config/
    config.exs           # Compile-time configuration
    dev.exs              # Development overrides
    prod.exs             # Production overrides
    runtime.exs          # Runtime configuration
  test/                  # Test files
  mix.exs                # Project definition and dependencies

The Context Boundary

Phoenix enforces a clear separation between your business logic (lib/my_app/) and your web interface (lib/my_app_web/). The business logic layer contains your domain – schemas, queries, and business rules. The web layer contains everything HTTP-specific – controllers, components, routers, and plugs. This separation means your core logic is reusable and testable independent of the web layer.

The Endpoint

The endpoint is the entry point for every HTTP request. It is a Plug pipeline that handles concerns like static file serving, request parsing, session management, and logging before the request ever reaches your router.

defmodule MyAppWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app

  # Serve static files from priv/static
  plug Plug.Static,
    at: "/",
    from: :my_app,
    gzip: false,
    only: MyAppWeb.static_paths()

  # Code reloading in development
  if code_reloading? do
    socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
    plug Phoenix.LiveReloader
    plug Phoenix.CodeReloader
  end

  plug Plug.RequestId
  plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]

  plug Plug.Parsers,
    parsers: [:urlencoded, :multipart, :json],
    pass: ["*/*"],
    json_decoder: Phoenix.json_library()

  plug Plug.MethodOverride
  plug Plug.Head
  plug Plug.Session, @session_options
  plug MyAppWeb.Router
end

The endpoint is started as part of your application’s supervision tree, which means it is automatically supervised and restarted if anything goes wrong.

The Router

The router maps incoming HTTP requests to the correct controller action. Phoenix routers use a declarative DSL that reads naturally:

defmodule MyAppWeb.Router do
  use MyAppWeb, :router

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

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", MyAppWeb do
    pipe_through :browser

    get "/", PageController, :home
    resources "/posts", PostController
    live "/dashboard", DashboardLive
  end

  scope "/api", MyAppWeb.API, as: :api do
    pipe_through :api

    resources "/articles", ArticleController, only: [:index, :show, :create]
  end
end

The resources macro generates all standard RESTful routes:

IEx
iex> MyAppWeb.Router.Helpers.post_path(MyAppWeb.Endpoint, :index)
"/posts"
iex> MyAppWeb.Router.Helpers.post_path(MyAppWeb.Endpoint, :show, 42)
"/posts/42"
iex> MyAppWeb.Router.Helpers.post_path(MyAppWeb.Endpoint, :new)
"/posts/new"
iex> MyAppWeb.Router.Helpers.post_path(MyAppWeb.Endpoint, :edit, 42)
"/posts/42/edit"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>Note
Phoenix 1.7+ encourages verified routes over the older route helpers. Verified routes use the ~p sigil and are checked at compile time: ~p"/posts/#{post}". They catch broken links before your code ever runs.

Plugs

Plugs are the fundamental building block of Phoenix’s request processing. A plug is any module that implements init/1 and call/2, or simply a function that takes a connection and options. Everything in Phoenix – from the endpoint to the router to authentication – is built on plugs.

# Module plug -- reusable across your app
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 log in to access this page.")
      |> redirect(to: ~p"/login")
      |> halt()
    end
  end
end
# Function plug -- quick inline usage
defmodule MyAppWeb.PostController do
  use MyAppWeb, :controller

  plug :require_owner when action in [:edit, :update, :delete]

  defp require_owner(conn, _opts) do
    post = Repo.get!(Post, conn.params["id"])

    if post.user_id == conn.assigns.current_user.id do
      assign(conn, :post, post)
    else
      conn
      |> put_flash(:error, "Not authorized")
      |> redirect(to: ~p"/posts")
      |> halt()
    end
  end
end

Controllers

Controllers handle incoming requests, coordinate with your business logic, and return responses. Each controller action receives the conn (a Plug.Conn struct) and params (a map of request parameters):

defmodule MyAppWeb.PostController do
  use MyAppWeb, :controller

  alias MyApp.Blog

  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

  def create(conn, %{"post" => post_params}) do
    case Blog.create_post(post_params) do
      {:ok, post} ->
        conn
        |> put_flash(:info, "Post created successfully.")
        |> redirect(to: ~p"/posts/#{post}")

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, :new, changeset: changeset)
    end
  end

  def delete(conn, %{"id" => id}) do
    post = Blog.get_post!(id)
    {:ok, _post} = Blog.delete_post(post)

    conn
    |> put_flash(:info, "Post deleted successfully.")
    |> redirect(to: ~p"/posts")
  end
end

Notice how the controller is thin – it delegates to Blog context functions for all business logic. The controller’s only job is to translate between HTTP and your application.

Components and Templates

Phoenix 1.7+ uses function components for rendering HTML. Components are plain Elixir functions that accept assigns and return HEEx (HTML + Embedded Elixir) templates:

defmodule MyAppWeb.PostHTML do
  use MyAppWeb, :html

  embed_templates "post_html/*"

  attr :post, MyApp.Blog.Post, required: true

  def post_card(assigns) do
    ~H"""
    <article class="post-card">
      <h2><%= @post.title %></h2>
      <p><%= @post.summary %></p>
      <time datetime={@post.inserted_at}>
        <%= Calendar.strftime(@post.inserted_at, "%B %d, %Y") %>
      </time>
      <.link navigate={~p"/posts/#{@post}"}>Read more</.link>
    </article>
    """
  end
end

Components are composable. You can call one component from another using the <.component_name /> syntax. Phoenix ships with a CoreComponents module that provides buttons, forms, tables, modals, and more out of the box.

The Request Lifecycle

Every Phoenix request flows through a predictable pipeline:

  1. Endpoint – Static files, parsing, sessions, security headers
  2. Router – Matches the URL and HTTP method to a pipeline and action
  3. Pipeline – A series of plugs that prepare the connection (e.g., :browser adds CSRF protection)
  4. Controller – Receives the prepared connection, calls business logic, and renders a response
  5. Component/Template – Generates the HTML (or JSON) response body

At each stage, the %Plug.Conn{} struct is transformed and passed along. If any plug calls halt(), the pipeline stops and the current response is sent back to the client.

JSON APIs

Phoenix makes it straightforward to build JSON APIs. Use Phoenix.Controller.json/2 or define a JSON component:

defmodule MyAppWeb.API.ArticleController do
  use MyAppWeb, :controller

  alias MyApp.Blog

  def index(conn, _params) do
    articles = Blog.list_articles()
    json(conn, %{data: Enum.map(articles, &article_json/1)})
  end

  def show(conn, %{"id" => id}) do
    article = Blog.get_article!(id)
    json(conn, %{data: article_json(article)})
  end

  defp article_json(article) do
    %{
      id: article.id,
      title: article.title,
      body: article.body,
      published_at: article.published_at
    }
  end
end
IEx
iex> Phoenix.json_library()
Jason
iex> Jason.encode!(%{hello: "world"})
"{\"hello\":\"world\"}"
iex> Jason.decode!("{\"hello\":\"world\"}")
%{"hello" => "world"}

Useful Mix Tasks

Phoenix provides several generators and utility tasks that accelerate development:

# Generate a complete HTML resource (context, schema, migration, controller, templates)
mix phx.gen.html Blog Post posts title:string body:text published:boolean

# Generate a JSON API resource
mix phx.gen.json Blog Article articles title:string body:text

# Generate a LiveView resource
mix phx.gen.live Blog Post posts title:string body:text

# Generate only a context and schema (no web layer)
mix phx.gen.context Blog Post posts title:string body:text

# List all routes
mix phx.routes

# Start an interactive console with your app loaded
iex -S mix phx.server

Build a Phoenix JSON API

Create a new Phoenix project and build a simple JSON API for a bookshelf application:

  1. Generate a new project with mix phx.new bookshelf --no-html --no-assets
  2. Create a Library context with a Book schema that has title, author, and isbn fields
  3. Define API routes under /api/books that support listing all books and showing a single book
  4. Write a controller that returns JSON responses
  5. Add a custom plug that logs the request path and method to the console

Bonus: Add a POST /api/books endpoint that validates the incoming data and returns appropriate error responses (422 for validation errors, 201 for successful creation).

Summary

Phoenix gives you a productive, convention-driven framework that leverages the BEAM’s strengths. The architecture – endpoint, router, pipelines, controllers, components – provides clear separation of concerns while keeping request processing fast and transparent. Plugs compose cleanly at every layer, and the context pattern keeps your business logic independent from your web interface. In the next lesson, you will explore LiveView, which builds on Phoenix to deliver rich, real-time user interfaces without writing JavaScript.

FAQ and Troubleshooting

Why is my Phoenix Framework example failing even though the code looks right?

Most failures come from runtime context, not syntax: incorrect app configuration, missing dependencies, process lifecycle timing, or environment-specific settings. Re-run with smaller examples, inspect intermediate values, and verify each prerequisite from this lesson before combining patterns.

How do I debug this topic in a production-like setup?

Start with reproducible local steps, add structured logs around boundaries, and isolate one moving part at a time. Prefer deterministic tests for the core logic, then layer integration checks for behavior that depends on supervisors, networked services, or external systems.

What should I optimize first?

Prioritize correctness and observability before performance tuning. Once behavior is stable, profile the hot paths, remove unnecessary work, and only then introduce advanced optimizations.

Related Lessons

Key Takeaways

  • You can explain the core ideas in this lesson and when to apply them in Elixir projects
  • You can use the primary APIs and patterns shown here to build working solutions
  • You can spot common mistakes for this topic and choose more idiomatic approaches