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:
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_sessionreads the session cookie,:protect_from_forgeryinstalls CSRF protection). Plugs are the Phoenix middleware unit. -
A pipeline groups a list of plugs under a name (here
:browserand:api). Ascopeblock then sayspipe_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:
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.
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. |
defmodule DemoWeb.ProductHTML do
use DemoWeb, :html
embed_templates "product_html/*"
end
<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
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:
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:
<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. |
The route list:
$ mix phx.routes
GET / DemoWeb.PageController :home
GET /products/:id DemoWeb.ProductController :show
Link to the next product
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:
<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. |
Multi-level paths
Nothing stops you from using longer paths. Route them the same way:
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:
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.