Check out the Alchemist's Edition

#50: Phoenix Routes

Phoenix 1.3


One of the first things you’ll probably do when building a new Phoenix application is to define some routes.

In this episode we’ll look at how we can use the Phoenix to create different routes for our application.

Let’s start by creating a new Phoenix application named teacher and we’ll install the dependencies.

Then change into the directory and create our database:

$ mix phx.new teacher
$ cd teacher
$ mix ecto.create

Now let’s see what routes Phoenix gives us to start.

We can run $ mix phx.routes to print out the defined routes.

Running it, we see there’s one route defined a get request that’s handled by our PageController index action, page_path GET / TeacherWeb.PageController :index.

Lets see how it’s defined in our routers. We’ll open our router.ex and we can see where Phoenix defined our route.

It’s making a GET request at “/“ which will then be handled by the index function in our PageController, which maps to what we saw when we listed our routes get "/", PageController, :index.

Now that we see where our current route is defined, let’s walk through the rest of the module so we have an understanding of what’s going on.

At the top of our module, the first thing is use TeacherWeb, :router. This let’s us use Phoenix’s router functions in our application’s router.

Then we see a pipeline for the browser. A pipeline allows us to group a common set of plugs together that we want to use for a certain type of request.

Our browser pipeline, for example, uses plugs that are always executed for browser requests like fetching the session, getting flash messages, and protecting against cross site request forgery or CSRF.

Now once we have our pipeline built up, we can run our connection through it by calling pipe_through. In addition to our browser pipeline Phoenix also creates a api pipeline.

By default it just checks that the request is JSON. But if we wanted to create another Plug that, for example, verified an API token, this would be a great place to include it.

If you’re unfamiliar with how plugs work, you can learn more about them in episodes 19 and episode 20.

Now the scope allows us to organize our routes by grouping them together. Here it’s taking a path (“/“) and an alias, in our case TeacherWeb

Now that we have a basic understanding of what’s happening let’s define some new routes. Let’s create an route that will accept a post request We’ll use the post function, give it a path, in this we’ll just use “/create”, the controller PageController and the function on the controller we want to call - :create.

Now we’ll need to define our create function in order to handle our new route. Let’s open our PageController and we’ll define our create function.

Our create function will take the connection and the params. We’ll ignore our params here since we won’t use them in our function.

In an actual create we’d probably insert some kind of record, for our example we’ll pretend it’s doing something.

Then let’s display a message to let the user know everything succeeded and we’ll render our index template.

lib/teacher_web/controllers/page_controller.ex

...
def create(conn, _params) do
  :timer.sleep(1_000) #create something
  
  conn
  |> put_flash(:info, "Your record was created")
  |> render("index.html")
end
...

Let’s go back to the command line and list all our routes again:

$ mix phx.routes

We see our new route is listed and it’s using the correct method of post and our PageController and create function.

Now let’s add link for our new route in a template. We’ll open our app.html.eex template.

And we’ll create a new link with the text “Create”. We want this link to go to our new route, so let’s use page_path helper, passing it our connection and the atom :create and because this is not a get but a post request we’ll need to specify the method.

Great, now let’s start up our server.

$ mix phx.server

And in our browser, let’s click our new link. We see our flash message displayed - great our new route is working.

Let’s go back to our router.ex and if we now wanted to add more routes, like one for put or delete we could do these one-by-one, however an easier way would be to define all the RESTful routes for our resource at once.

We can do exactly that with the resources function. Let’s see what that would look like for a “movies” resource.

We’d call resources provide a path, in this case “/movies”, then we’ll give it a controller, MovieController.

lib/teacher_web/router.ex

...
resources "/movies", MovieController
...

And if we go to the command line and list our routes.

$ mix phx.routes
  movie_path  GET     /movies           TeacherWeb.MovieController :index
  movie_path  GET     /movies/:id/edit  TeacherWeb.MovieController :edit
  movie_path  GET     /movies/new       TeacherWeb.MovieController :new
  movie_path  GET     /movies/:id       TeacherWeb.MovieController :show
  movie_path  POST    /movies           TeacherWeb.MovieController :create
  movie_path  PATCH   /movies/:id       TeacherWeb.MovieController :update
              PUT     /movies/:id       TeacherWeb.MovieController :update
  movie_path  DELETE  /movies/:id       TeacherWeb.MovieController :delete

We see the new routes for our movies are listed. Now one common option is to limit the routes created when using the resources function. For example, if we wanted to create all of our RESTful routes except, :delete, we can pass the option expect with a list of actions to exclude, in this case :delete. except: [:delete].

lib/teacher_web/router.ex

...
resources "/movies", MovieController, except: [:delete]
...

And if we ran our routes again, the DELETE action wont be included.

$ mix phx.routes
  movie_path  GET    /movies           TeacherWeb.MovieController :index
  movie_path  GET    /movies/:id/edit  TeacherWeb.MovieController :edit
  movie_path  GET    /movies/new       TeacherWeb.MovieController :new
  movie_path  GET    /movies/:id       TeacherWeb.MovieController :show
  movie_path  POST   /movies           TeacherWeb.MovieController :create
  movie_path  PATCH  /movies/:id       TeacherWeb.MovieController :update
              PUT    /movies/:id       TeacherWeb.MovieController :update

The resources function also lets us use nested routes.

Let’s say wanted a route to list the actors for a movie, one way we could do that is to create an “/actors” route that’s nested in our “/movies” resource.

We’ll it to use a do block and define a resource for our actors, giving it a path “/actors” and an ActorController. Then let’s limit this to only create the :index action.

To do that we’ll use another option, only and give it a list of the actions we want to create, in this case :index. From the command line, we’ll list our routes again. And we see our new “/actors” route is listed.

$ mix phx.routes
  movie_path  GET    /movies                   TeacherWeb.MovieController :index
  movie_path  GET    /movies/:id/edit          TeacherWeb.MovieController :edit
  movie_path  GET    /movies/new               TeacherWeb.MovieController :new
  movie_path  GET    /movies/:id               TeacherWeb.MovieController :show
  movie_path  POST   /movies                   TeacherWeb.MovieController :create
  movie_path  PATCH  /movies/:id               TeacherWeb.MovieController :update
              PUT    /movies/:id               TeacherWeb.MovieController :update
  movie_actor_path  GET    /movies/:movie_id/actors  TeacherWeb.ActorController :index

Let’s look at one last feature of Phoenix routes - creating a wildcard route. Let’s have our new route handle get requests and by using the asterisk, it will catch any route that comes in.

We’ll append “thing”, allowing us to access the requested path in the params under the key “thing” and let’s handle this in our PageController with a function we’ll call wildcard: get "/*thing", PageController, :wildcard.

Then let’s open our page_controller.ex and let’s write our function.

Inside it we’ll get value of the matched path that will be under the key “thing”. Then let’s display it with a flash message and we’ll render our “index.html” template.

lib/teacher_web/controllers/page_controller.ex

...
def wildcard(conn, params) do
  path = Map.fetch!(params, "thing")

  conn
  |> put_flash(:info, "Caught this route - #{path}")
  |> render("index.html")
end
...

Let’s try it out. We’ll go to the browser and type in a route we haven’t defined: http://localhost:4000/nothing/

And great our path “nothing” is displayed in our message.

This also works for a url like this: http://localhost:4000/nothing/is/here

But now if we go back to our homepage we see our message is still displayed.

And if we check our development server logs, we see a message telling us that our wildcard will always match.

So let’s go back to our router.ex. It will always match because it’s at the top of the scope and let’s move our wildcard route it to the bottom.

Then let’s try our homepage again - and great it’s back to rendering as it should.

Now instead of matching all routes, let’s say we only wanted our wildcard route to match if the path starts with a specific pattern, say “phoenix”.

We’ll put what we want to match on just before our asterisk: get "/phoenix*thing", PageController, :wildcard.

Let’s try putting “phoenix” at the beginning of our path: http://localhost:4000/phoenix-is-great

Great - it caught our route.

Let’s try another route: http://localhost:4000/phoenix/is/great

Perfect it works for that as well. Then if we try a route that doesn’t exist and is not prefixed by “phoenix” - we get a no route error.

And just to be sure - our homepage still works.