Phoenix API versioning: URL

Imaging you are building an API to a mobile application. This app allow users to create and update their profiles. One of the features of this app, is to allow the user to set her/his “favorite_sport”.

Now imagine the first version of the app was released and thousands of people started using it. It’s a huge success and with success, the product team discovers that many users are not satisfied with the limitation of providing only one favorite sport. Instead, they want to provide a list of their favorite sports.

With that in mind, product and tech teams get together and define the strategy to support multiple “favorite_sports”. This feature will be released on the second version of the app.

There is another issue, though. How to keep the users of the first version of the application (that only supports one favorite_sport) and the users of the new version of the application (that supports a list of favorite_sports) happy?

API Versioning

API versioning allows you to response with different content, based on the information sent by the client. As detailed by feature request example above, it’s very likely that new features (or updates to existent features) will happen, making it really hard to keep your API backward compatible.

Allowing clients to send information about supported versions, frees the development team to evolve the API (and the product team to not be limited to the API design), delivering specific features to clients that are aware about how to handle them.

How to use versions?

There are a number of approaches that can be used when dealing with versioned APIs. The most common are:

This post will talk about the latter.

The API

Getting back to example we used above, we can imagine our API looks like:

First version (v1) including only one “favorite_sport”:

$ curl http://api.app.tld/v1/users/971

{
  "id": 971,
  "name": "John Doe",
  "email": "john@doe.com",
  "favorite_sport": "Soccer"
}

Second version (v2) including a list of “favorite_sports”:

$ curl http://api.app.tld/v2/users/971

{
  "id": 971,
  "name": "John Doe",
  "email": "john@doe.com",
  "favorite_sports": [
    "Soccer",
    "Tennis"
  ]
}

Routing

Let’s start with the Routing. We want to be able to handle v1 and v2 requests, sending them to the right controller.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
defmodule ActivityTracker.Router do
  use ActivityTracker.Web, :router
  alias ActivityTracker.APIVersion

  pipeline :v1 do
    plug :accepts, ["json"]
    plug APIVersion, version: :v1
  end

  pipeline :v2 do
    plug :accepts, ["json"]
    plug APIVersion, version: :v2
  end

  scope "/v1", ActivityTracker do
    pipe_through :v1

    resources "/users", UserController
  end

  scope "/v2", ActivityTracker do
    pipe_through :v2

    resources "/users", UserController
  end
end

Considering the code above, if you run mix phoenix.routes, you will see something like:

$ mix phoenix.routes

...
user_path  GET     /v1/users/:id       ActivityTracker.UserController :show
...
user_path  GET     /v2/users/:id       ActivityTracker.UserController :show
...

Both v1 and v2 will be dispatched to the ActivityTracker.UserController and the show action.

When we perform a request to v1, we end up on line 15 (yes, many things happen before touching this line, but bear with me). The first thing we have inside this scope is pipe_through :v1 on line 16, that runs the pipeline we defined on line 5. This pipeline has two plugs:

The first one says that the server is capable of rendering json (accepts/2). The second one is a custom plug we defined with the following code:

defmodule ActivityTracker.APIVersion do
  import Plug.Conn

  def init(opts), do: opts

  def call(conn, opts) do
    version = Keyword.fetch!(opts, :version)
    assign(conn, :version, version)
  end
end

It basically sets the version value inside the connection assigns to the value specified by the option :version, that’s passed to the plug. On line 7, the :version option is :v1.

Getting back to line 15, now we know that before dispatching the request to the UserController, all requests of this scope (/v1/...) will have a version value set in the connection assigns.

The exactly same thing happens to requests to the /v2/... (on line 21). The difference is that we run the v2 pipeline, and this sets the version assign to v2.

Controller

The router did all the “hard-work” for us. If someone tried to request to /v3/..., a non-supported version, the request wouldn’t get to this point. It means that, if requests are getting here (in the UserController), it’s either asking for v1 or v2. With this information, we can simply use “pattern-matching” to correctly handle different versions:

1
2
3
4
5
6
7
8
9
10
11
12
13
defmodule ActivityTracker.UserController do
  use ActivityTracker.Web, :controller
  alias ActivityTracker.User

  def show(%{assigns: %{version: :v1}}=conn, %{"id" => id}) do
    user = Repo.get(User, id)
    render(conn, "show.v1.json", user: user)
  end
  def show(%{assigns: %{version: :v2}}=conn, %{"id" => id}) do
    user = Repo.get(User, id)
    render(conn, "show.v2.json", user: user)
  end
end

Line 5 pattern-matches for version: :v1, being responsible for all v1 requests. Line 10 does the same with v2 requests. For that example, the only difference between line 5 and 10 is the view that is rendered. On line 7 we render show.v1.json and on line 11 we render show.v2.json. That’s one approach of rendering different views, but not the only one.

View

If we got to this point, it means that we are about to render something. The controller has decided about rendering show.v1.json or show.v2.json, based on the versions requested by the client. The views are the simplest part of this entire flow, applying pattern-matching once again:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
defmodule ActivityTracker.UserView do
  use ActivityTracker.Web, :view

  def render("show.v1.json", %{user: user}) do
    %{
      id: user.id,
      name: user.name,
      email: user.email,
      favorite_sport: Enum.at(user.favorite_sports, 0)
    }
  end
  def render("show.v2.json", %{user: user}) do
    %{
      id: user.id,
      name: user.name,
      email: user.email,
      favorite_sports: user.favorite_sports
    }
  end
end

On line 4 we handle v1, sending the response with the single “favorite_sport” value (line 9). On line 12 we handle the v2, sending the response with the list of “favorite_sports” values (line 17).

Conclusion

You can see that pattern-matching helped us a lot in that example. The code is simple and clear, making it easy to every one to understand what’s happening and the path that the code is following. It also helped us to focus on what really matters: we handle v1 and v2, if something different comes through, we have nothing to do with that (we don’t write code for cases we are not expecting).

I hope this post can give you some idea about API versions with Phoenix. It’s only one example of what can be achieved.

Are you developing an API using Phoenix? Send me an email and let me know how you are versioning it.

You might also like

Free book

Today I released "Versioned APIs with Phoenix" free book. It covers three different strategies on API versioning with Phoenix...

Phoenix API versioning: Accept Header

This is the second part of the API versioning series. This post shows how to achieve API versioning using the Accept header...

Deploying Elixir releases

I have seen great posts about Elixir release deployments lately and I would like to share my experience deploying Elixir releases to production...

Download free e-book

Learn different strategies on API versioning with "Versioned APIs with Phoenix" free e-book