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!