Router

We have used Phoenix.Router plenty of times in Phoenix Basics and you already understand the concept. So this is just a small chapter with some useful details.

All code in this chapter will result form this base application:

$ mix phx.new demo --no-ecto --no-dashboard (1)
$ cd demo
1 We don’t include the Phoenix.LiveDashboard (--no-dashboard) to keep our router setup tidy. Otherwise, a million routes would leave just to the real-time performance monitoring dashboard, which is not a topic in this chapter.

This results in a minimal router setup:

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

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    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, :index
  end
end

For us, relevant is just the last / part.

Display existing routes

With the command mix phx.routes you get a list of all defined routes. In our vanilla demo system this is the output:

$ mix phx.routes
Compiling 13 files (.ex)
Generated demo app
page_path  GET  /                    DemoWeb.PageController :index (1)
websocket  WS   /socket/websocket    DemoWeb.UserSocket (2)
1 This route is for a HTTP GET request for the root path - which gets matched by / - of our application. It triggers the :index action in the DemoWeb.PageController controller.
2 No need to discuss websockets in this chapter. I will remove that line in following exampless to clean up the output.
In more complex applications with a dashboard, the output of mix phx.routes gets quite a bit longer. Then mix phx.routes | grep "whatever-you-search" becomes a convenient tool on the command line.

Params

Often you have some sort of ID in the URL or other params you need to access in your application. And sometimes you want that to handle that in the router.

We create a product view of our demo application to show how it’s working. We have to touch/create three files to make this happen:

lib/demo_web/router.ex
[...]

scope "/", DemoWeb do
  pipe_through :browser

  get "/", PageController, :index
  get "/products/:id", ProductController, :show (1)
end

[...]
1 We ask the router to match everything at that position to the parameter :id.
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)
    |> render("show.html")
  end
end
1 We match the id in the params.
lib/demo_web/views/product_view.ex
defmodule DemoWeb.ProductView do
  use DemoWeb, :view
end
lib/demo_web/templates/product/show.html.eex
<h1>ID: <%= @id %></h1> (1)
1 Just a simple display. Normally we would have fetched the product with that id from our database and display it.

If you open http://localhost:4000/products/1 with your browser you’ll see this log entry:

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

And just as a final proof the screenshot:

http://localhost:4000/products/1

Query String

Query Strings don’t belong in the router but are handled very similarly to the above params in the controller.

Let’s assume we want to add the query string "color=blue" to our product request: http://localhost:4000/products/1?color=blue

We can’t match for it in the router but Phoenix does realize what happens:

[info] GET /products/1
[debug] Processing with DemoWeb.ProductController.show/2
  Parameters: %{"color" => "blue", "id" => "1"} (1)
  Pipelines: [:browser]
[info] Sent 200 in 600µs
1 Both parameter become part of the params map.

Without doing anything in the router, the query string parameter color finds it’s way into the controller. But we have to change the controller a bit:

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) (2)
    |> render("show.html")
  end

  def show(conn, %{"id" => id}) do (3)
    conn
    |> assign(:id, id)
    |> render("show.html")
  end
end
1 This show/2 function matches if there is an :id and a :color parameter.
2 In addition to :id we have to assign :color too.
3 This show/2 function matches if there is only an :id parameter.
The order of the show/2 functions in the controller is significant. If we use the other order for this specific example the %{"id" ⇒ id, "color" ⇒ color} would never match because %{"id" ⇒ id} always matches first if it’s the first function.

Lastly we have to change the template:

lib/demo_web/controllers/product_controller.ex
<h1>ID: <%= @id %></h1>

<%= if assigns[:color] do %> (1)
<p>Color: <%= @color %></p>
<% end %>
1 Because we call this template from two different functions we have to take care of the case when the color assign hasn’t taken place. The alternative would be a different template for each function.
http://localhost:4000/products/1?color=blue

A view on the routes:

$ mix phx.routes
Compiling 1 file (.ex)
   page_path  GET  /                                      DemoWeb.PageController :index
product_path  GET  /products/:id                          DemoWeb.ProductController :show

Assuming you’d like to link to the next product ID this would be the template:

lib/demo_web/controllers/product_controller.ex
<h1>ID: <%= @id %></h1>

<%= if assigns[:color] do %> (1)
<p>Color: <%= @color %></p>
<% end %>

<%= link "Next", to: Routes.product_path(@conn, :show, String.to_integer(@id) + 1) %> (1)
1 We use the DemoWeb.ProductController :show route and add the id to it. To increase the current @id we have to call String.to_integer/1 first.

Assuming you’d like to link to the first product with the query "color=orange" you’d have to use this code:

lib/demo_web/controllers/product_controller.ex
<h1>ID: <%= @id %></h1>

<%= if assigns[:color] do %> (1)
<p>Color: <%= @color %></p>
<% end %>

<%= link "First product in orange", to: Routes.product_path(@conn, :show, 1, color: "orange") %> (1)
1 This results in a link to http://localhost:4000/products/1?color=orange

Multilevel Paths

In the previous easy code examples, we always put the routes on the first level. But of course, you can use sublevel paths too. Here’s an example:

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

  get "/an-other-test/abc/def/", PageController, :index
end
[...]

No surprise here:

$ mix phx.routes
Compiling 1 file (.ex)
page_path  GET  /an-other-test/abc/def    DemoWeb.PageController :index

Wildcards

You don’t have to define the route precisely. You can use the * wildcard too.

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

  get "/names/st*an", PageController, :index
end
[...]

This route matches /names/stefan and /names/stephan:

[info] GET /names/stefan
[debug] Processing with DemoWeb.PageController.index/2
  Parameters: %{"an" => ["stefan"]} (1)
  Pipelines: [:browser]
[info] Sent 200 in 4ms
[info] GET /names/stephan
[debug] Processing with DemoWeb.PageController.index/2
  Parameters: %{"an" => ["stephan"]}
  Pipelines: [:browser]
[info] Sent 200 in 1ms
1 The parameter an which is automatically named as the last match segment contains the whole match. You can use that to to some interesting things.

Misc

There’s no need to replicate the official documentation in this chapter. By now, you understand the concept of Phoenix.Router. You’ll find solutions for all the exceptional cases which are not handled in this chapter in the official documentation at https://hexdocs.pm/phoenix/Phoenix.Router.html