---
title: Redirecting routes in a Phoenix application using plugs
teaser: "Let's explore a pragmatic, flexible, and testable approach to route redirection.
  \n"
tags: web,elixir,phoenix
author: Wil Hall
published_on: 2020-10-21
---

There are many reasons we might want to redirect routes in our phoenix
application such as the name of a domain concept changing or the removal of a
page from the application.

As an example, let's say we have the following route `/home` and we want to
change it to `/welcome`:

```elixir
scope "/", AppWeb do
  pipe_through :browser

  get "/home", HomeController, :index
end
```

One of the most common solutions &mdash; and perhaps the most straightforward
&mdash; is to create a `WelcomeController`, rename the view module and
template file, and redirect from the old controller:

```elixir
defmodule AppWeb.HomeController do
  use AppWeb, :controller

  def index(conn, _params) do
    welcome_path = Routes.welcome_path(conn, :index)

    conn |> redirect(to: welcome_path) |> halt()
  end
end
```

And then we can reference both controllers from our `router.ex`:

```elixir
scope "/", AppWeb do
  pipe_through :browser

  get "/home", HomeController, :index
  get "/welcome", WelcomeController, :index
end
```

Functionally, there is nothing wrong with this approach. It gets the job done,
and it's simple to implement. But there are a some significant downsides:

1. We have to keep both the old controller and the new controller around
1. If our redirection behavior were even slightly more complex &mdash;
   such as handling a URL parameter &mdash; it would feel like the old
   controller knew too much about the new controller's implementation
1. Someone looking at `router.ex` can't know whether `get "/home",
   HomeController, :index` is rendering a page that exists, or performing a
   redirection
1. We can't test the redirection behavior without writing a controller test

The solution to these problems is to extract the redirection logic and
responsibility away from the controller.

Where should that logic live? In a [Plug], of course!

## Redirecting a single route

[Plug]: https://thoughtbot.com/blog/testing-elixir-plugs

The [`Phoenix.Router.forward/4`] macro allows us to forward requests made for a
given path to a named plug; for example:

[`Phoenix.Router.forward/4`]: https://hexdocs.pm/phoenix/Phoenix.Router.html#forward/4

```elixir
scope "/", AppWeb do
  pipe_through :browser

  get "/welcome", WelcomeController, :index
  forward "/home", Plugs.WelcomePageRedirector
end
```

Even without knowing the implementation details of `Plugs.WelcomePageRedirector`, it is still clear _from a quick look looking at the route definition_ that the `/home` route is performing a redirect.

If we then define `Plugs.WelcomePageRedirector` in
`app/lib/app_web/plugs/welcome_page_redirector.ex`, a simple implementation
might look like this:

```elixir
defmodule AppWeb.Plugs.WelcomePageRedirector do
  alias AppWeb.Router.Helpers, as: Routes

  def init(default), do: default

  def call(conn, _opts) do
    welcome_path = Routes.welcome_path(conn, :index)

    conn
    |> Phoenix.Controller.redirect(to: welcome_path)
    |> Plug.Conn.halt()
  end
end
```

The implementation of the plug is almost identical to how we previously
implemented the redirect in the controller. But encapsulating the logic into a
plug means that we can easily [test the plug] in isolation from the
controller.

[test the plug]: https://thoughtbot.com/blog/testing-elixir-plugs

## Redirecting multiple routes

If you have multiple routes you want to redirect, a single plug for each route
is a viable option. And there is value in keeping the plug implementations
straightforward to make them easier to understand and test.

But in situations where you have a set of routes you want to redirect in a
predictable way, you can write a more dynamic plug. 

As an example, let's say we have a `ProfileController` that handles all
requests to the route `/profile/:id/*` and we want to rename it to
`UserController` and move its routes to `/user/:id/*`.

The `forward/4` macro forwards all requests _starting with_ the given path, so
the following would handle any route starting with `/profile`:

```elixir
scope "/", AppWeb do
  pipe_through :browser

  resources "/user", UserController 
  forward "/profile", Plugs.ProfileRedirector
end
```

And `Plugs.ProfileRedirector` has access to the [request fields from
`Plug.Conn`], allowing us to dynamically redirect the request in our plug
implementation based on the user id path parameter:

```elixir
defmodule AppWeb.Plugs.ProfileRedirector do
  alias AppWeb.Router.Helpers, as: Routes

  def init(default), do: default

  def call(conn, _opts) do
    [id, _tail] = conn.path_info

    user_path = Routes.user_path(conn, :show, id)

    conn
    |> Phoenix.Controller.redirect(to: user_path)
    |> Plug.Conn.halt()
  end
end
```

[request fields from `Plug.Conn`]: https://hexdocs.pm/plug/Plug.Conn.html#module-request-fields

The above implementation _does not_ attempt to make all resource routes work.
Instead, it simply redirects them to the `:show` page under the new `/user` URL
path.

This is a trade-off. We could write a complex plug that allows all requests to
the old route `/profile/:id/*` to function as if they were made to
`/user/:id/*`, but we can avoid that complexity if our use case is just to
redirect lost users to the new route.

Ideally we want to keep these plugs short and simple, ensuring that they are
easy to understand and easy to test.
