dnlgrv

Versioning your HTTP API - Elixir example

If you’re interested in why this example is here, this is the post it originated from: Versioning your HTTP API


Elixir apps tend to use the Plug module to handle requests, so what we’re going to do here is use a Plug to direct our incoming request to a module, depending on the requested API version.

I’m going to demonstrate this within a Phoenix application (called Example), but it will work for any Plug-based application.

Our API will have two versions. Each version will have their own Router. You can put whatever routes you like in those:

defmodule Example.V1.Router do
  use Example.Web, :router

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", Example.V1 do
    pipe_through :api
    get "/users", UserController, :index
  end
end

defmodule Example.V2.Router do
  use Example.Web, :router

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", Example.V2 do
    pipe_through :api
    get "/users", UserController, :index
    get "/comments", CommentController, :index
  end
end

To route the incoming request to the V1.Router or V2.Router, we need to create a Plug. Here’s a basic Plug for what we need, called Plug.VersionRouter:

defmodule Plug.VersionRouter do
  import Plug.Conn

  @prefix "application/vnd.my_app."

  def init(opts), do: opts

  def call(conn, opts) do
    default_version = Keyword.fetch!(opts, :default_version)
    versions = Keyword.fetch!(opts, :routers)

    api_version =
      conn
      |> get_req_header("accept")
      |> List.first()
      |> parse_version_header()

    router = case api_version do
      nil ->
        Dict.get(versions, default_version)
      _ ->
        Dict.get(versions, api_version)
    end

    router.call(conn, router.init([])) # now the router is in control
  end

  defp parse_version_header(@prefix <> version), do: version
  defp parse_version_header(_), do: nil # no API version provided
end

The Plug takes some options, such as the default version, and the mapping of versions to routers. All that’s left to do is plug it in (get it?) to our pipeline. We can do that by replacing the last line of the lib/example/endpoint.ex module, so rather than going to the default router Phoenix created for us, our VersionRouter plug is handling it.

# lib/example/endpoint.ex
# ...
plug Example.VersionRouter,
  default_version: "v2",
  versions: %{
    "v1" => Example.V1.Router,
    "v2" => Example.V2.Router
  %}

This Plug is by no means bullet-proof. For example, it doesn’t handle what happens if someone requests an invalid API version.

While writing this post I decided to create and publish a Plug for this very purpose. Here it is: dnlgrv/plug_media_type_router. It’s a little further along than the example above, and will continue to be improved. If you want to help out, please contribute!