README.md

# 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](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.

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

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

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

```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).