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
scope "/", AppWeb do pipe_through :browser get "/home", HomeController, :index end
One of the most common solutions — and perhaps the most straightforward
— is to create a
WelcomeController, rename the view module and
template file, and redirect from the old controller:
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
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:
- We have to keep both the old controller and the new controller around
- If our redirection behavior were even slightly more complex — such as handling a URL parameter — it would feel like the old controller knew too much about the new controller’s implementation
- Someone looking at
router.excan’t know whether
get "/home", HomeController, :indexis rendering a page that exists, or performing a redirection
- 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!
Phoenix.Router.forward/4 macro allows us to forward requests made for a
given path to a named plug; for example:
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
app/lib/app_web/plugs/welcome_page_redirector.ex, a simple implementation
might look like this:
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.
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
forward/4 macro forwards all requests starting with the given path, so
the following would handle any route starting with
scope "/", AppWeb do pipe_through :browser resources "/user", UserController forward "/profile", Plugs.ProfileRedirector end
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:
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
The above implementation does not attempt to make all resource routes work.
Instead, it simply redirects them to the
:show page under the new
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.