Routing in Phoenix Umbrella Apps
Tags: elixir, learning
Published: 2019-04-16 12:37:24

---
title: Routing in Phoenix Umbrella Apps
published: true
description: Today, we’re diving into Phoenix umbrella applications to build a router that can route requests from multiple subdomains to different apps in our umbrella. 

cover_image: https://blog.appsignal.com/images/blog/2019-04/umbrella.jpg
canonical_url: https://blog.appsignal.com/2019/04/16/elixir-alchemy-routing-phoenix-umbrella-apps.html
tags: #elixir, #learning
---

[Umbrella apps](https://elixirschool.com/en/lessons/advanced/umbrella-projects/) are an awesome way to structure Elixir projects.

Behind the curtains, they are a very thin layer that just compiles everything to a single package. Instead of building a single large monolith, you can structure your code with multiple isolated contexts. They all get compiled and run under the same BEAM instance, so they still have access to each other. Meanwhile the conceptual separation ensures you have separate OTP apps for each of your umbrella children. And it allows you to work on each of them with a certain degree of isolation. 

Think of this as a poor man’s microservices solution. You don’t need to add a messaging queue or send HTTP requests between each service since they’re all actually running under the same process, but you still get some of the benefits.

If you want to know more about umbrella applications, I suggest [the official guide](https://elixir-lang.org/getting-started/mix-otp/dependencies-and-umbrella-projects.html#umbrella-projects) as a starter, as it clearly outlines the advantages and caveats of umbrella apps.

Now let's look at a real life example where I've implemented an umbrella app. 

## A Real Example

Let’s say I’m building a website for [Magic: The Gathering (MTG)](https://en.wikipedia.org/wiki/Magic:_The_Gathering) cards. Which… well, [I am](https://github.com/naps62/mse). The idea is to create an interface where users can browse and search a database of cards. There’s also an admin panel where some administrative tasks can be performed.

Clearly, each of these frontend interfaces has different requirements:

1. The main frontend is public while the admin side only has private access.
2. The admin panel may even have its own UI requirements. In this case, I’m using [ex_admin](https://github.com/smpallen99/ex_admin) for convenience. This means, even UI assets are not shared.
3. They mostly have completely different back-end logic as well. Only a small subset of the queries and operations can be shared between the two.
4. I may also want to access both of them through different URLs (e.g. use an `admin` subdomain for the Admin frontend).

The multiple differences between the two make it clear that it would be better for these to be two separate phoenix apps—each with its own setup.

Something like this:

```ruby
apps/
  client/
  admin/
  shared/
```

## Looks Easy Enough. What’s the Issue?

The problem comes when you try to figure out how to actually implement this. How do you route requests from the `admin` subdomain to another Phoenix app while routing other requests to the main Phoenix app?

One solution would be to run each of those apps on a different port. But then, you'll either be left accessing `admin.mydomain.com:4001`, or you’ll need some other middle layer to abstract away that port distinction from your browser. While this may be fine for an admin page that only you will access, it doesn’t work as well for a general solution.

The old school solution is to put a reverse proxy between your clients and your server. nginx does this job pretty well. But in reality, you know all this is a single Elixir application. It seems weird to need a third party server to be able to route requests to different parts of it.

It also doesn’t solve the problem of local development, unless you want to run nginx locally as well, which is less than ideal.

We’re Elixir developers after all, and we’re pretty smart. So let’s do this the Elixir way:

## Introducing a Proxy App

The solution I came up with (i.e. read suggestions from similar use cases on Stack Overflow) was to create an additional umbrella child, which will be the main point of contact to the outside world.

This app, which we’ll call `proxy`, will receive all incoming HTTP requests and forward them to the appropriate Phoenix app, based on a few simples rules. In our simple use case, requests to `admin.mydomain.com` will be forwarded to the `admin` app, and all others will be forwarded to the `client` app.

This is a very simple phoenix app, which you can generate with `mix phx.new` like all the others. Dependencies will be kept to a minimum here. We only have phoenix & cowboy as external dependencies (to set up our web server), as well as the client and admin apps to which we’ll be forwarding requests:

```ruby
def deps do
  [
    {:client, in_umbrella: true},
    {:admin, in_umbrella: true},
    {:phoenix, "~> 1.3.2"},
    {:cowboy, "~> 1.0"}
  ]
end
```

Since this app will be the actual web server, we should disable the server setting in the other two:

```ruby
# apps/client/config/config.exs
config :client, Client.Web.Endpoint, server: false

# apps/admin/config/config.exs
config :admin, Admin.Web.Endpoint, server: false
    
# apps/proxy/config/config.exs
config :proxy, Proxy.Endpoint, server: true
```

This ensures that only the proxy app will be listening to a port. This is not mandatory but it saves you the trouble of having to define different ports for each one (remember: only one listener per port is allowed) and ensures all requests actually go through the proxy app—which is indeed the expected behavior.

Leaving `server: true` might be useful in development or testing mode, depending on how you want to set up your environment.

## Setting up the Endpoint

The entry point of a Phoenix app is the `Endpoint` module. In this case, we’ve set this to `Proxy.Endpoint`. Since this app really has no other responsibility, there’s no need to nest it under the `Web` module, as is common practice in Phoenix.

However, we can strip down most things from the Endpoint module created for us by the Phoenix generator and end up with a very simple module:

```ruby
defmodule Proxy.Endpoint do
  use Phoenix.Endpoint, otp_app: :proxy
    
  @base_host_regex ~r|\.?mydomain.*$|
  @subdomains %{
    "admin" => Admin.Web.Endpoint,
    "client" => Client.Web.Endpoint
  }
  @default_host Client.Web.Endpoint
    
  def init(opts), do: opts
    
  def call(conn, _) do
    with subdomain <- String.replace(conn.host, @base_host_regex, ""),
         endpoint <- Map.get(@subdomains, subdomain, @default_host) do
      endpoint.call(conn, endpoint.init())
    end
  end
end
```

Let’s go over this one step at a time:

```ruby
@base_host_regex ~r|\.?mydomain.*$|
```

This is used to extract the subdomain part of the host URL of every request. So for `admin.mydomain.com` we want to get the string `admin` and for `mydomain.com` we will end up with an empty string (meaning, we’ll forward this to the default app. More on that later).

Notice that this doesn’t exactly match the `.com` part. This is a convenience change I made for local development. Matching on `mydomain.*` allows me to use `admin.mydomain.lvh.me` when working on my local machine, and still have this whole logic working without making development-specific changes.

If you don’t know what `lvh.me` is, [this article](https://nickjanetakis.com/blog/ngrok-lvhme-nipio-a-trilogy-for-local-development-and-testing#lvh-me) might be helpful (TL;DR: It’s a development service that resolves its name to `localhost`).

With the above regex in mind, the next part should be easy to understand:

```ruby
@subdomains %{
    "admin" => Admin.Web.Endpoint,
    "client" => Client.Web.Endpoint
}
@default_host Client.Web.Endpoint
```

For every subdomain, we want to match a particular Phoenix endpoint belonging to the app that we want to forward the request to. `@default_host` is what we’ll use if the subdomain is missing (the empty string scenario we talked above).

```ruby
def call(conn, _) do
    with subdomain <- String.replace(host, @base_host_regex, ""),
         endpoint <- Map.get(@subdomains, subdomain, @default_host) do
      endpoint.call(conn, endpoint.init())
    end
end
```

When this endpoint—which is actually not much more than an [Elixir Plug](https://hexdocs.pm/plug/readme.html)—is called, we just grab the subdomain from the request host, then find the matching endpoint from our mapping (defaulting to `@default_host`), and call `endpoint.call/2` on it. This is essentially delegating the call down to the appropriate app.

Now `client` and `admin` both have to only worry about their corresponding requests and authentication. All logic related to the multiple subdomains & clients we may need is abstracted away in this app.

Want a new client in the same umbrella? Add it here! Want the same endpoint to respond to additional subdomains? Add it here!

## Taking the routing even further

By adding a smart router to our umbrella application, we’re now able to serve requests to different subdomains to different apps in our umbrella application. I first implemented this pattern on [a pet project of mine](https://github.com/naps62/mse/tree/master/apps/proxy), but have since used and improved it on a few production projects as well.

We could take this much further. For example, if you’re migrating an existing service from Ruby to Elixir. You can have this proxy application route all requests made to the Ruby version of your service redirected back to the Ruby application, ensuring backward-compatibility. Or you may want the opposite scenario, where you’re creating a new API service and want to forward matching requests to a different client or even to a different web server altogether.

We can also take the routing complexity to another level. Routing was done here based solely on the subdomain of the request. But depending on your needs, you can create more complex routing rules using HTTP headers or query parameters. All of this can be done while keeping your actual web services completely oblivious to it.

We had a blast 💥thinking about all the possibilities of Umbrellas and Routing. We hope it set your mind on 🔥fire as well. If you want to get a regular dose of ⚗️Elixir Alchemy, [subscribe](https://appsignal.com/elixir-alchemy) to get the next episode of Alchemy delivered straight to your inbox.

*This post is written by guest author [Miguel Palhas](http://naps62.github.io/). Miguel is a professional over-engineer [@subvisual](https://twitter.com/subvisual) and organizes [@rubyconfpt](https://twitter.com/rubyconfpt) and [@MirrorConf](https://twitter.com/mirrorconf).*