Router

We have already used Phoenix.Router a handful of times in the Phoenix Basics chapter, so the basic idea should be familiar by now. This short chapter fills in a few details: listing all routes, capturing URL parameters, handling query strings, and using the modern verified routes (the ~p sigil) instead of old-style helper functions.

Phoenix 1.8 encourages verified routes via the ~p sigil: a simple ~p"/users/#{user}" replaces the older Routes.user_path(conn, :show, user) helper, checks the route against the router at compile time, and fails early if the path does not exist. We use this style throughout the book.

All the examples in this chapter run against this base application:

$ mix phx.new demo --no-ecto --no-dashboard (1)
$ cd demo
1 We skip the Phoenix LiveDashboard so the route list stays short.

The generated router looks like this:

lib/demo_web/router.ex
defmodule DemoWeb.Router do
  use DemoWeb, :router

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

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

  scope "/", DemoWeb do
    pipe_through :browser

    get "/", PageController, :home
  end
end

Two concepts appear in that file that we have only glanced at so far:

  • A plug is a small, reusable step that transforms the connection (for example, :fetch_session reads the session cookie, :protect_from_forgery installs CSRF protection). Plugs are the Phoenix middleware unit.

  • A pipeline groups a list of plugs under a name (here :browser and :api). A scope block then says pipe_through :browser, which runs every request matched inside that scope through the plugs in the named pipeline.

For this chapter, the scope "/" do … end block is all that matters. Each line inside it registers one route. We revisit pipelines briefly in the Scopes and pipelines section at the end of this chapter.

Listing existing routes

mix phx.routes prints every route the router knows about. For the default --no-dashboard project the output is small:

$ mix phx.routes
  GET   /                    DemoWeb.PageController :home (1)
  *     /dev/mailbox         Plug.Swoosh.MailboxPreview [] (2)
  WS    /live/websocket      Phoenix.LiveView.Socket (3)
  GET   /live/longpoll       Phoenix.LiveView.Socket
  POST  /live/longpoll       Phoenix.LiveView.Socket
1 The root path GET / calls the :home action on DemoWeb.PageController.
2 Swoosh’s development mailbox viewer for previewing outgoing email. Only mounted in dev.
3 The LiveView transport endpoints. Nothing to worry about right now, they power LiveView’s real-time features.
In bigger apps the route list grows quickly. mix phx.routes | grep <needle> narrows it down. For example, mix phx.routes | grep ProductController shows only routes handled by ProductController.

Path Params

A URL often needs a slot for some identifier such as /products/42. You express this in the router with a :<name> segment:

lib/demo_web/router.ex
  scope "/", DemoWeb do
    pipe_through :browser

    get "/", PageController, :home
    get "/products/:id", ProductController, :show (1)
  end
1 Anything at that position in the URL is captured into the parameter named :id.

Now let us write the matching controller, HTML module, and template.

lib/demo_web/controllers/product_controller.ex
defmodule DemoWeb.ProductController do
  use DemoWeb, :controller

  def show(conn, %{"id" => id}) do (1)
    conn
    |> assign(:id, id) (2)
    |> render(:show)
  end
end
1 Pattern-match the "id" key out of the params map.
2 In a real app you would fetch a product from the database; for now we just pass the ID straight to the template.
lib/demo_web/controllers/product_html.ex
defmodule DemoWeb.ProductHTML do
  use DemoWeb, :html

  embed_templates "product_html/*"
end
lib/demo_web/controllers/product_html/show.html.heex
<h1 class="text-2xl font-bold">ID: {@id}</h1>

Open http://localhost:4000/products/1 in your browser. The log entry looks like this:

[info] GET /products/1
[debug] Processing with DemoWeb.ProductController.show/2
  Parameters: %{"id" => "1"}
  Pipelines: [:browser]
[info] Sent 200 in 373µs
http://localhost:4000/products/1

Query Strings

Query strings (?foo=bar) do not need anything in the router. Phoenix merges them into the same params map. If the user visits http://localhost:4000/products/1?color=blue, params becomes %{"id" ⇒ "1", "color" ⇒ "blue"}.

We can match on the color by adding a more specific clause before the catch-all:

lib/demo_web/controllers/product_controller.ex
defmodule DemoWeb.ProductController do
  use DemoWeb, :controller

  def show(conn, %{"id" => id, "color" => color}) do (1)
    conn
    |> assign(:id, id)
    |> assign(:color, color)
    |> render(:show)
  end

  def show(conn, %{"id" => id}) do (2)
    conn
    |> assign(:id, id)
    |> render(:show)
  end
end
1 This clause matches only when both id and color are present.
2 This clause handles the plain id case.
Order matters. Elixir picks the first clause that matches. If the id-only clause came first, the two-parameter version would never run (%{"id" ⇒ id} matches any map that has an "id" key, including one that also has "color").

Now update the template to display the color when it is set:

lib/demo_web/controllers/product_html/show.html.heex
<h1 class="text-2xl font-bold">ID: {@id}</h1>

<p :if={assigns[:color]}> (1)
  Color: {@color}
</p>
1 :if is a HEEx attribute that renders the element only if the expression is truthy. assigns[:color] gracefully returns nil when the assign is missing (rather than raising), which is exactly what we want here since one of the two clauses does not set :color.
http://localhost:4000/products/1?color=blue

The route list:

$ mix phx.routes
  GET   /                     DemoWeb.PageController :home
  GET   /products/:id         DemoWeb.ProductController :show

To link to the next product by ID, use the ~p sigil. Phoenix interpolates the value and verifies the path against the router at compile time:

lib/demo_web/controllers/product_html/show.html.heex
<h1 class="text-2xl font-bold">ID: {@id}</h1>

<p :if={assigns[:color]}>Color: {@color}</p>

<.link href={~p"/products/#{String.to_integer(@id) + 1}"}> (1)
  Next
</.link>
1 ~p"/products/#{…}" builds /products/2 when @id is "1". No string concatenation, no typos, and your editor can jump to the route definition.

Query parameters are a keyword list at the end of the sigil interpolation:

lib/demo_web/controllers/product_html/show.html.heex
<.link href={~p"/products/1?#{[color: "orange"]}"}>
  First product in orange
</.link>

This renders as <a href="/products/1?color=orange">….

Multi-level paths

Nothing stops you from using longer paths. Route them the same way:

lib/demo_web/router.ex
  scope "/", DemoWeb do
    pipe_through :browser

    get "/an-other-test/abc/def", PageController, :home
  end
$ mix phx.routes
  GET   /an-other-test/abc/def    DemoWeb.PageController :home

Wildcards (glob segments)

Phoenix supports glob matching with *. Matching everything from a given point in the URL is handy for "catch the rest" routes:

lib/demo_web/router.ex
  scope "/", DemoWeb do
    pipe_through :browser

    get "/names/*name", PageController, :show_name (1)
  end
1 *name captures every remaining URL segment into a list called name.

Visit /names/stefan and the controller sees %{"name" ⇒ ["stefan"]}. Visit /names/stefan/wintermeyer and it sees %{"name" ⇒ ["stefan", "wintermeyer"]}. Join the list back into a string with Enum.join(segments, "/") if you need the original path.

Scopes and pipelines

Everything above lived in one scope, but in bigger apps you split routes by section and by the middleware they need. A scope groups routes that share a URL prefix and a module namespace; a pipeline groups the plugs those routes should run through:

  scope "/", DemoWeb do
    pipe_through :browser        # HTML pages

    get "/", PageController, :home
  end

  scope "/api", DemoWeb.API do
    pipe_through :api            # JSON-only pipeline

    get "/products/:id", ProductController, :show
  end

Phoenix 1.8 also introduces scopes for secure data access (the authentication generator leans on them heavily). You will see scope blocks appear again in the authentication chapter.

Where to go next

The router has more features than we can fit here, things like resources/3 (which generates seven CRUD routes at once), forward/2 (for mounting plug apps), and live session scopes. The official documentation is excellent: https://hexdocs.pm/phoenix/Phoenix.Router.html.