Subscribe to access all episodes. View plans →
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.