README.md

# PhoenixApiVersions

> ### Move your API forward. Support legacy versions with ease.
> PhoenixApiVersions helps Phoenix JSON API apps support legacy versions while minimizing maintenance overhead.

[![Master](https://travis-ci.org/smartrent/phoenix_api_versions.svg?branch=master)](https://travis-ci.org/smartrent/phoenix_api_versions)
[![Hex.pm Version](http://img.shields.io/hexpm/v/phoenix_api_versions.svg?style=flat)](https://hex.pm/packages/phoenix_api_versions)
[![Coverage Status](https://coveralls.io/repos/github/smartrent/phoenix_api_versions/badge.svg?branch=master)](https://coveralls.io/github/smartrent/phoenix_api_versions?branch=master)

## Documentation

API documentation is available at [https://hexdocs.pm/phoenix_api_versions](https://hexdocs.pm/phoenix_api_versions)

## How Does It Work?

### It's A JSON Translation Layer

PhoenixApiVersions simply does the following:

1. Modifies incoming JSON before it reaches the controller
2. Modifies outgoing JSON right before sending the response

### Versions Are Defined In Layers

```
-------------
|           |
|    v3     |
| (current) |
|           |
|     ▲     |
------|------  <-- v2/v3 translation layer
|     ▼     |
|           |
|    v2     |
|           |
|     ▲     |
------|------  <-- v1/v2 translation layer
|     ▼     |
|           |
|    v1     |
|           |
-------------
```

Each legacy version is responsible for transforming JSON to and from the shape expected/returned by the next version. **Apart from bug fixes, developers will only have to maintain middleware from the last version.**

Assume an API whose **current version is v3**.

- v1 middleware transforms incoming JSON to the shape that v2 expects.
- v2 middleware transforms incoming JSON to the shape that v3 expects.

The request reaches the controller in the shape of the current version. The controller and view respond with "v3 JSON".

- v2 middleware transforms outgoing v3 JSON to the shape that v2 should return.
- v1 middleware then transforms the v2 JSON to the shape that v1 should return.

Once v4 comes out, developers will simply build the transformation layer for v3-to-v4 (and back).

### Supports Any Versioning Mechanism

The version can be specified in any way:

- URL (`/api/v1/...`)
- Accept header (`Accept: application/vnd.github.v3.json`)
- Custom header (`X-Api-Version: 2016-01-20`)
- Anything else in `conn`

### Benefits

#### ✅ Limits Legacy Code

PhoenixApiVersions only allows developers to define old versions by **transforming JSON**.

It assumes that these JSON-transforming middleware functions will not perform database calls or heavy computation. (Although this is not completely prohibited.)

#### ✅ Flexible

If your application has one or two legacy API endpoints that simply need to be handled differently, that's completely posslble.

#### ✅ Ensure Consistent Business Rules Across API Versions

Every version of a given API endpoint will reach the same controller function, making it much less likely that subtle differences between business rules will crystallize over time.

## Installation

### Add PhoenixApiVersions To `web.ex`

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.

```elixir
# web.ex

def controller do
  quote do

    plug PhoenixApiVersions.Plug

  end
end

def view do
  quote do

    use PhoenixApiVersions.View


    # Optional; recommended if you have no other way to handle 404's yet
    def render("404.json", _) do
      %{error: "not_found"}
    end

  end
end
```

### Create an ApiVersions 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`.

#### Example

```elixir
# 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
```

### Add ApiVersions Module in `config.exs`

Reference this module in your Phoenix application's `config.exs` as such:

```elixir
config :phoenix_api_versions, versions: MyApp.ApiVersions
```

### Add 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:

```elixir
# 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:

- Stripe's API versioning scheme [revealed in this blog](https://stripe.com/blog/api-versioning).
- [This Hacker News comment](https://news.ycombinator.com/item?id=16445698) by [bringtheaction](https://news.ycombinator.com/user?id=bringtheaction) which references an idea from a [Rich Hickey talk](https://www.youtube.com/watch?v=oyLBGkS5ICk) about "maintaining old versions not by backporting bug fixes but instead by rewriting the old version to be a thin layer that gives you the interface of the old version upon the code of the new version."

## License

This software is licensed under [the MIT license](LICENSE.md).