How to split a router into multiple modules using Phoenix

This is a common question when using Phoenix and when searching the web I didn’t find any complete examples. I ended up experimenting for myself and I figured I might as well share my findings.
You essentially have two options.
- Because a router is a plug, you can forward/4 to another router. However it does some things you might not expect. The
path_info
in theconn
gets changed. In the example below/one
is removed frompath_info
. We’ll go into more detail on that below! There’s another issue: according to the Phoenix docs, you may encounter a bug where plugs defined by your app and the forwarded endpoints get invoked twice.
Note: The path_info
is the url after the TLD split by slashes, so for a url example.com/user/1/profile
the path info would be [“user”, “1”, “profile"]
.
For the sake of learning I’ll demonstrate how to split a router using forward
. Below I demonstrate making these changes to the router of a freshly generated a phoenix application:
scope "/", AppWeb do
pipe_through :browser
get "/", PageController, :index
forward “/one”, RouterOne
end
And then create the new router module like this:
defmodule AppWeb.RouterOne do
use Phoenix.Router
get “/”, AppWeb.ControllerOne, :index
end
Now http://localhost:4000/one takes you to that route. Assuming you’re using the port 4000…and that you’re capable of running your app locally.
2. The often preferable solution is to match/5 within the router. This doesn’t appear to have any side effects. It merely directs the request to the other router. It has the added benefit of being able to use the first parameter to match some HTTP verbs, for instance get
, to a specific router.
Replace forward with:
match(:*, “/one/”, RouterOne, [])
Note: The first :*
parameter means match all HTTP methods.
Change RouterOne
a tiny bit to accommodate:
get “/”, AppWeb.ControllerOne, :index
→
get “/one”, AppWeb.ControllerOne, :index
Voila! You have split out your router with match
.
If you inspect the difference in the conn
between both approaches you’ll notice 4 differences: (at least using Phoenix
version 1.5.8
).
The conn contains a lot of data but it’s this specifically that differs from match
when you use forward
:
path_info: [],
script_name: ["one"],
private: %{
AppWeb.Router => {[], %{AppWeb.RouterOne => [“one”]}},
AppWeb.RouterOne => {[“one”], %{}},
And if you use match
it looks something like this:
path_info: [“one”],
script_name: [],
private: %{
AppWeb.Router => {[], %{}},
AppWeb.RouterOne => {[], %{}},
Match preserves path_info
and forward
moves it into private
in a way that’s hard to use and make sense of which is in my opinion another downside.
There’s also a script_name
key that gets introduced. It is the initial portion of a URLs path that corresponds to the application routing.
If you look at the source code for match
not too much is happening.
defmacro match(verb, path, plug, plug_opts, options \\ []) do
add_route(:match, verb, path, plug, plug_opts, options)
end
However when you look at forward there’s a lot more going on.
defmacro forward(path, plug, plug_opts \\ [], router_opts \\ []) do
plug = Macro.expand(plug, %{__CALLER__ | function: {:init, 1}})
router_opts = Keyword.put(router_opts, :as, nil)
quote unquote: true, bind_quoted: [path: path, plug: plug] do
plug = Scope.register_forwards(__MODULE__, path, plug)
unquote(add_route(:forward, :*, path, plug, plug_opts, router_opts))
end
end
You can see that the router_opts
get changed. But there’s a lot more to it. But wait, there’s more! But it’s outside of the scope of this post. tl;dr: there is much more to forward than one might assume.
If you aren’t convinced you should be using match
yet, forward
also manages to do something quite confusing. If you run mix phx.routes
and you’re using forward you’ll see:
* /one PhoenixSplitRouteExampleWeb.RouterOne []
As opposed to match which very similarly to non forwarded routes, outputs:
router_one_path * /one PhoenixSplitRouteExampleWeb.RouterOne []
Which is actually maintainable. We don’t want to muck the output of mix phx.routes
. I believe it is safe to say that in most cases we want to use match/5
.
I hope you enjoyed this! If you’re interested I have a link to a pull request that demonstrates this code below.
Visit https://engineering.community.com/ for more great content by engineers at Community.