Subscribe to access all episodes. View plans →

#5: Versioned API with Phoenix

Published February 13, 2017

Elixir 1.3.4

Phoenix 1.2.1

TrailingFormatPlug 0.0.5

Episode source code on GitHub


Here we have a site that lists a few movies.

If we wanted to open up an API on this site for other applications to use, how would we do that in Phoenix?

If you want to follow along from this point, in the episode notes below I’ve included a link to a repo that you can clone and use.

So let’s get started.

We’ll start off by defining the routes we want.

Let’s open our router.ex

Then we’ll create a scope for our API

We’ll use the api pipeline that’s created by default with any new Phoenix app.

Now if you’re creating an API - it’s a good practice to version it - so let’s do that here.

We’ll start with version 1.

And we’ll use a movie resource for our api.

Our API will be read-only, so let’s limit it to only have the show and index actions.

web/router.ex

  scope "/api", Teacher do
    pipe_through :api

    scope "/v1", Api.V1 do
      resources "/movies", MovieController, only: [:index, :show]
    end
  end

Let’s take a look at our routes by running $ mix phoenix.routes from the command line.

movie_path  GET     /api/v1/movies      Teacher.Api.V1.MovieController :index
movie_path  GET     /api/v1/movies/:id  Teacher.Api.V1.MovieController :show
movie_path  GET     /movies             Teacher.MovieController :index
movie_path  GET     /movies/:id/edit    
...

While our routes have different endpoints, you can see they’re all named ‘movie_path’ - let’s update the name of our API routes to help them stand out.

Let’s add as: :api_v1 :

web/router.ex

  scope "/api", Teacher do
    pipe_through :api

    scope "/v1", Api.V1, as: :api_v1 do
      resources "/movies", MovieController, only: [:index, :show]
    end
  end

And if we go back to the terminal and run $ mix phoenix.routes again, we can see the names of our new api routes have been changed.

api_v1_movie_path  GET     /api/v1/movies      Teacher.Api.V1.MovieController :index
api_v1_movie_path  GET     /api/v1/movies/:id  Teacher.Api.V1.MovieController :show
movie_path  GET     /movies             Teacher.MovieController :index
movie_path  GET     /movies/:id/edit    
...

Let’s make one small change. We’ll remove :api from our ‘v1’ scope and move it to the ‘api’ scope. This will make our naming a little more focused.

web/router.ex

  scope "/api", Teacher, as: :api do
    pipe_through :api

    scope "/v1", Api.V1, as: :v1 do
      resources "/movies", MovieController, only: [:index, :show]
    end
  end

and if we run $ mix phoenix.routes again we see our routes are the same.

Great now we need to create the corresponding controller and view for our API.

Let’s start by creating an empty ‘movie_controller.ex’ in ‘controllers/api/v1/movie_controller.ex’

web/controllers/api/v1/movie_controller.ex

defmodule Teacher.Api.V1.MovieController do
  use Teacher.Web, :controller
end

Then we’ll create our empty ‘movie_view.ex’ in ‘views/api/v1/movie_view.ex’

web/views/api/v1/movie_view.ex

defmodule Teacher.Api.V1.MovieView do
  use Teacher.Web, :view
end

Back in our API’s ‘movie_controller.ex’, let’s start by defining the index action. We’ll query for all the movies in out database. Then we’ll call the render/3 function and since this is our api controller, we’ll use ‘index.json’ as the name to pattern match against in our corresponding view.

Then let’s define the show action. This time we’ll pattern match to get the ‘id’ from the ‘params’ map, then use that to find the corresponding movie from the database.

Then we’ll render ‘show.json’ which we’ll setup in the corresponding view along with ‘index.json’.

web/controllers/api/v1/movie_controller.ex

defmodule Teacher.Api.V1.MovieController do
  use Teacher.Web, :controller

  alias Teacher.Movie

  def index(conn, _params) do
    movies = Repo.all(Movie)
    render(conn, "index.json", movies: movies)
  end

  def show(conn, %{"id" => id}) do
    movie = Repo.get!(Movie, id)
    render(conn, "show.json", movie: movie)
  end

end 

Let’s do that now.

We’ll go to the empty movie view for our API.

I’ve completed this module off-screen, but let’s walk through it.

Let’s start off by looking at our render ‘movie.json’. This function is responsible for determining what fields from our movies we want to expose in our API. Here we can see it is returning a map with ‘id’, ‘title’, ‘summary’, and ‘year’ attributes.

Our other two functions are the corresponding render functions that are called from movie controller we just set up.

First is our render function for ‘index.json’, it pattern matches on ‘movies’. Then we’re returning a map, that has a key of ‘data’ with the value of the render_many/4 function. The ‘render_many’ function will return a list of our movies with the attritibutes defined in the render ‘movie.json’ function.

Our next function is render ‘show.json’,

Which pattern matches on our singular ‘movie’.

Again we return a map that has a ‘data’ key that returns the value of render_one/4.

‘render_one’ will return a single map again with our attributes defined in render ‘movie.json’

web/views/api/v1/movie_view.ex

defmodule Teacher.Api.V1.MovieView do
  use Teacher.Web, :view

  def render("index.json", %{movies: movies}) do
    %{data: render_many(movies, Teacher.Api.V1.MovieView, "movie.json")}
  end

  def render("show.json", %{movie: movie}) do
    %{data: render_one(movie, Teacher.Api.V1.MovieView, "movie.json")}
  end

  def render("movie.json", %{movie: movie}) do
    %{id: movie.id,
      title: movie.title,
      summary: movie.summary,
      year: movie.year}
  end

end

With our view done, let’s restart our server.

And if we go to our API’s ‘index’ action at localhost:4000/api/v1/movies, we can see all of our movies listed.

Now let’s check out our API’s show action at localhost:4000/api/v1/movies/1, and great we see the correct movie.

This looks good, but let’s make a few updates to our API.

We’ll start by removing this leading ‘data’ key.

Now let’s update what attributes we want to expose.

In the render ‘movie.json’ function - let’s remove the id and the summary.

web/views/api/v1/movie_view.ex

defmodule Teacher.Api.V1.MovieView do
  use Teacher.Web, :view

  def render("index.json", %{movies: movies}) do
    render_many(movies, Teacher.Api.V1.MovieView, "movie.json")
  end

  def render("show.json", %{movie: movie}) do
    render_one(movie, Teacher.Api.V1.MovieView, "movie.json")
  end

  def render("movie.json", %{movie: movie}) do
    %{title: movie.title,
      year: movie.year}
  end

end

Hitting the endpoint again you can see we’re only returning the attributes we specified and there’s no ‘data’ name in our JSON.

Many APIs, like those build with Ruby on Rails, support the trailing ‘.json’ on the url, but if we append that here, we get an error.

Luckily there’s a handy plug we can use to support just this.

Let’s open our mix.exs file and include ‘trainling_format_plug’ in the dependencies.

mix.exs

defp deps do
  ...
  {:trailing_format_plug, "~> 0.0.5"}
  ...
end

Then in our endpoint.ex we’ll include the ‘TrailingFormatPlug’.

lib/teacher/endpoint.ex

...
  plug TrailingFormatPlug
  plug Plug.RequestId
  plug Plug.Logger
...

Now we can fetch the dependencies and restart the server.

$ mix deps.get

$ mix phoenix.server

And if we go back to the browser, we can see that our API is working again.

© 2024 HEXMONSTER LLC