Slide 1

Slide 1 text

Versioning APIs in Phoenix 1 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 2

Slide 2 text

Why Version Your APIs? 2 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 3

Slide 3 text

Software Evolves 3 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 4

Slide 4 text

Legacy clients. → Integration is expensive. → Sometimes it does not make business sense. → Backwards compatibility is part of providing a good service. 4 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 5

Slide 5 text

Mobile Apps 5 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 6

Slide 6 text

Example 6 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 7

Slide 7 text

FoRealEstate 7 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 8

Slide 8 text

Real-Estate App Scenario: User can set neighborhood of interest ----------------------------------------------- Given I am a User And I am viewing my profile When I select neighborhood of interest Then I am able to search and select a location from a list 8 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 9

Slide 9 text

Product manager comes knocking Scenario: User can set neighborhood(s) of interest -------------------------------------------------- Given I am a User And I am viewing my profile When I select neighborhoods of interest Then I am able to search and select multiple neighborhoods 9 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 10

Slide 10 text

Old request body GET /user/profile { "user": { "id": 1, "full_name": "Johnny Appleseed", "search_neighborhood": "West Loop" } } 10 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 11

Slide 11 text

New request body GET /user/profile { "user": { "id": 1, "full_name": "Johnny Appleseed", "search_neighborhoods": ["West Loop", "West Town", "Ukrainian Village"] } } 11 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 12

Slide 12 text

API versioning strategies → request parameter based → URL based → headers based (Accept) 12 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 13

Slide 13 text

Anatomy of a web request in Phoenix 13 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 14

Slide 14 text

%Plug.Conn{} → request headers → response headers → body params → cookies → assigns → ... 14 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 15

Slide 15 text

defmodule FoRealEstate.Router do use Phoenix.Router pipeline :browser do plug :fetch_session plug :accepts, ["html"] end scope "/" do pipe_through :browser # browser related routes and resources end end 15 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 16

Slide 16 text

Function Plug Example def put_headers(conn, key_values) do Enum.reduce key_values, conn, fn {k, v}, conn -> Plug.Conn.put_resp_header(conn, to_string(k), v) end end 16 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 17

Slide 17 text

This is how we use it defmodule HelloPhoenix.MessageController do use HelloPhoenix.Web, :controller plug :put_headers, %{content_encoding: "gzip", cache_control: "max-age=3600"} plug :put_layout, "bare.html" # ... end 17 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 18

Slide 18 text

Module Plug Example defmodule HelloPhoenix.Plugs.Locale do import Plug.Conn @locales ["en", "fr", "de"] def init(default), do: default def call(%Plug.Conn{params: %{"locale" => loc}} = conn, _default) when loc in @locales do assign(conn, :locale, loc) end def call(conn, default), do: assign(conn, :locale, default) end 18 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 19

Slide 19 text

Usage defmodule HelloPhoenix.Router do use HelloPhoenix.Web, :router pipeline :browser do plug :accepts, ["html"] # ... plug HelloPhoenix.Plugs.Locale, "en" end # ... 19 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 20

Slide 20 text

Back to our example Here is our controller defmodule FoRealEstate.UserController do use FoRealEstate.Web, :controller alias FoRealEstate.User def profile(%{assigns: %{version: :v1}}=conn, _params) do user = User.build(:v1) render conn, "profile.v1.json", user: user end def profile(%{assigns: %{version: :v2}}=conn, _params) do user = User.build(:v2) render conn, "profile.v2.json", user: user end end 20 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 21

Slide 21 text

the view defmodule FoRealEstate.UserView do use FoRealEstate.Web, :view def render("show.v1.json", %{user: user}) do %{ id: user.id, full_name: user.full_name, neighborhood: Enum.at(user.neighborhoods, 0) } end def render("show.v2.json", %{user: user}) do %{ id: user.id, full_name: user.full_name, neighborhoods: user.neighborhoods } end end 21 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 22

Slide 22 text

Versioning using a request parameter GET /user/profile?version=v1 { "user": { "id": 1, "full_name": "Johnny Appleseed", "search_neighborhood": "West Loop" } } 22 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 23

Slide 23 text

Versioning using a request parameter GET /user/profile?version=v2 { "user": { "id": 1, "full_name": "Johnny Appleseed", "search_neighborhoods": ["West Loop", "West Town", "Ukrainian Village"] } } 23 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 24

Slide 24 text

web/router.ex defmodule FoRealEstate.Router do use FoRealEstate.Web, :router pipeline :api do plug :accepts, ["json"] plug FoRealEstate.Version, %{"v1" => :v1, "v2" => :v2} end scope "/", FoRealEstate do pipe_through :api get "/user/profile", UserController, :profile end end 24 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 25

Slide 25 text

web/version.ex defmodule FoRealEstate.Version do import Plug.Conn def init(versions), do: versions def call(%{params: %{"version" => version}} = conn, versions) do assign(conn, :version, Map.fetch!(versions, version)) end end 25 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 26

Slide 26 text

Versioning using URL GET v1/user/profile GET v2/user/profile 26 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 27

Slide 27 text

web/router.ex defmodule FoRealEstate.Router do use FoRealEstate.Web, :router pipeline :v1 do plug :accepts, ["json"] plug FoRealEstate.Version, version: :v1 end pipeline :v2 do plug :accepts, ["json"] plug FoRealEstate.Version, version: :v2 end scope "/v1", FoRealEstate do pipe_through :v1 get "/user/profile", UserController, :profile end scope "/v2", FoRealEstate do pipe_through :v2 get "/user/profile", UserController, :profile end end 27 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 28

Slide 28 text

defmodule FoRealEstate.Version do import Plug.Conn def init(opts), do: opts def call(conn, opts) do assign(conn, :version, opts[:version]) end end 28 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 29

Slide 29 text

Subdomains? GET v1.example.com/user/profile GET v2.example.com/user/profile 29 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 30

Slide 30 text

No problem! scope "/", FoRealEstate, host: "v1." do pipe_through :v1 get "/user/profile", UserController, :profile end scope "/", FoRealEstate, host: "v2." do pipe_through :v2 get "/user/profile", UserController, :profile end 30 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 31

Slide 31 text

Versioning using headers GET /user/profile Accept: application/vnd.forealestate.v1+json GET /user/profile Accept: application/vnd.forealestate.v2+json 31 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 32

Slide 32 text

The Accept HTTP header The Accept request HTTP header advertises which content types, expressed as MIME types, the client is able to understand. — Mozilla Developer Network 32 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 33

Slide 33 text

config/config.exs config :plug, :mimes, %{ "application/vnd.forealestate.v1+json" => [:v1], "application/vnd.forealestate.v2+json" => [:v2] } 33 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 34

Slide 34 text

34 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 35

Slide 35 text

You will need to recompile plug: mix deps.clean plug --build && mix deps.get 35 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 36

Slide 36 text

web/version.ex defmodule FoRealEstate.Version do import Plug.Conn @versions Application.get_env(:plug, :mimes) def init(opts), do: opts def call(conn, _opts) do [accept] = get_req_header(conn, "accept") {:ok, [version]} = Map.fetch(@versions, accept) assign(conn, :version, version) end end 36 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 37

Slide 37 text

Then in our router... defmodule FoRealEstate.Router do use FoRealEstate.Web, :router pipeline :api do plug :accepts, [:v1, :v2] plug FoRealEstate.Version end scope "/", FoRealEstate do pipe_through :api get "/user/profile", UserController, :profile end end 37 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 38

Slide 38 text

Conclusion 38 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 39

Slide 39 text

Credits/Resources I learned all about API versioning from a book called Versioned APIs with Phoenix — Elvio Viscosa. 39 — Dorian Karter | ChicagoElixir | @dorian_escplan

Slide 40

Slide 40 text

Learn more about plugs in Phoenix's official docs: http://www.phoenixframework.org/docs/ understanding-plug 40 — Dorian Karter | ChicagoElixir | @dorian_escplan