PhoenixApiVersions

PhoenixApiVersions helps Phoenix applications support multiple JSON API versions while minimizing maintenance overhead.

Documentation

API documentation is available at https://hexdocs.pm/phoenix_api_versions

Getting Started

Adding PhoenixApiVersions To Your Project

In the Phoenix web.ex file for your JSON API, add the plug to the controller section, and use the PhoenixApiVersions view macro in the view section.

Optionally, you may want to add a render("404.json", _) function in the view section, which can be used later if you don’t already have a mechanism for handling 404’s.

# web.ex

def controller do
  quote do
    # ...

    plug PhoenixApiVersions.Plug

    # ...
  end
end

def view do
  quote do
    # ...

    use PhoenixApiVersions.View

    def render("404.json", _) do
      %{error: "not_found"}
    end

    # ...
  end
end

Creating an ApiVersions Module

Create a configuration module. We suggest calling this ApiVersions, namespaced inside your phoenix application’s main namespace. (e.g. MyApp.ApiVersions)

Make sure to use PhoenixApiVersions in this module.

The module must implement the PhoenixApiVersions behaviour, which includes version_not_found/1, version_name/1, and versions/0.

# lib/my_app_web/api_versions/api_versions.ex

defmodule MyApp.ApiVersions do
  use PhoenixApiVersions

  alias PhoenixApiVersions.Version
  alias MyApp.ApiVersions.V1
  alias Plug.Conn
  alias Phoenix.Controller

  def version_not_found(conn) do
    conn
    |> Conn.put_status(:not_found)
    |> Controller.render("404.json", %{})
  end

  def version_name(conn) do
    Map.get(conn.path_params, "api_version")
  end

  def versions do
    [
      %Version{
        name: "v1",
        changes: [
          V1.ChangeNameToDescription,
          V1.AnotherChange
        ]
      },
      %Version{
        name: "v2",
        changes: []
      }
    ]
  end
end

Creating Change Modules

Change modules are only used when the current route is found in routes/1.

Example

Assume your project has a concept of devices, each with a name property. In version v2, you want to change name to description.

Simply change all your code (and the database field) to description. Then, implement a change like this:

# lib/my_app_web/api_versions/v1/change_name_to_description.ex

defmodule MyApp.ApiVersions.V1.ChangeNameToDescription do
  use PhoenixApiVersions.Change

  alias MyApp.Api.DeviceController

  def routes do
    [
      {DeviceController, :show},
      {DeviceController, :create},
      {DeviceController, :update},
      {DeviceController, :index}
    ]
  end

  def transform_request_body_params(%{"name" => _} = params, DeviceController, action)
      when action in [:create, :update] do
    params
    |> Map.put("description", params["name"])
    |> Map.drop(["name"])
  end

  def transform_response(%{data: device} = output, DeviceController, action)
      when action in [:create, :update, :show] do
    output
    |> Map.put(:data, device_output_to_v1(device))
  end

  def transform_response(%{data: devices} = output, DeviceController, :index) do
    devices = Enum.map(devices, &device_output_to_v1/1)

    output
    |> Map.put(:data, devices)
  end

  defp device_output_to_v1(device) do
    device
    |> Map.put(:name, device.description)
    |> Map.drop([:description])
  end
end

As a result, v1 API endpoints will accept and return the field as name, while v2 API endpoints will accept and return is as description.

Credits

The inspiration for this library came from two sources:

License

This software is licensed under the MIT license.