Versioning APIs in Phoenix

Versioning APIs in Phoenix

Talk given in @ChicagoElixir meetup based on a book by Elvio Viscosa

0e752ec9121eb5ebc9924f5b2e4b788e?s=128

Dorian Karter

January 25, 2017
Tweet

Transcript

  1. 4.

    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
  2. 8.

    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
  3. 9.

    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
  4. 10.

    Old request body GET /user/profile { "user": { "id": 1,

    "full_name": "Johnny Appleseed", "search_neighborhood": "West Loop" } } 10 — Dorian Karter | ChicagoElixir | @dorian_escplan
  5. 11.

    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
  6. 12.

    API versioning strategies → request parameter based → URL based

    → headers based (Accept) 12 — Dorian Karter | ChicagoElixir | @dorian_escplan
  7. 13.

    Anatomy of a web request in Phoenix 13 — Dorian

    Karter | ChicagoElixir | @dorian_escplan
  8. 14.

    %Plug.Conn{} → request headers → response headers → body params

    → cookies → assigns → ... 14 — Dorian Karter | ChicagoElixir | @dorian_escplan
  9. 15.

    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
  10. 16.

    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
  11. 17.

    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
  12. 18.

    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
  13. 19.

    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
  14. 20.

    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
  15. 21.

    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
  16. 22.

    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
  17. 23.

    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
  18. 24.

    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
  19. 25.

    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
  20. 27.

    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
  21. 28.

    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
  22. 30.

    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
  23. 31.

    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
  24. 32.

    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
  25. 35.

    You will need to recompile plug: mix deps.clean plug --build

    && mix deps.get 35 — Dorian Karter | ChicagoElixir | @dorian_escplan
  26. 36.

    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
  27. 37.

    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
  28. 39.

    Credits/Resources I learned all about API versioning from a book

    called Versioned APIs with Phoenix — Elvio Viscosa. 39 — Dorian Karter | ChicagoElixir | @dorian_escplan