Subscribe to access all episodes. View plans →
Published February 26, 2019
Elixir 1.7
Phoenix 1.4
View source on GitHub
In this episode we’ll explore how we can use Phoenix to help us build a JSON API. We currently have a simple a simple Record Store application running that simply lists some popular albums, along with some information about each one. We can view them on our site, but what we’d like to do now is create an API so that other developers can have access to our albums.
Let’s get started. The first thing we’ll want to do is open our router.ex
and at the bottom of the file, we see that Phoenix provides an “api” scope to help get us started. Let’s uncomment so we can use it. Let’s keep our “api” in an namespace, so we’ll add Api
after TeacherWeb
. Doing this will allow us to put our api controller and view modules in an “api” directory, which will help keep things separate from our existing functionality. Then we’ll add our albums resource and our API will be read-only, so we’ll limit the actions to “show” and “index”.
lib/teacher_web/router.ex
...
scope "/api", TeacherWeb.Api do
pipe_through :api
resources "/albums", AlbumController, only: [:show, :index]
end
...
Now that we’ve defined our new routes, let’s check that they’re correct. We’ll go to the command line and run mix phoenix routes. This will show us all the routes we’ve defined for our application. At the bottom, we see our api routes.
While the route looks good, the path helper has the same name as the helper for our existing album routes.
$ mix phx.routes
album_path GET /albums TeacherWeb.AlbumController :index
album_path GET /albums/:id TeacherWeb.AlbumController :show
...
album_path GET /api/albums TeacherWeb.Api.AlbumController :index
album_path GET /api/albums/:id TeacherWeb.Api.AlbumController :show
Let’s go back to the router and we can specify the name of our helper with as: :api
- this will prefix our api routes with “api”.
lib/teacher_web/router.ex
...
scope "/api", TeacherWeb.Api, as: :api do
pipe_through :api
resources "/albums", AlbumController, only: [:show, :index]
end
...
Then if we go back to the command line and run mix phx.routes
again we see our api path helpers are being prefixed with “api”.
$ mix phx.routes
...
api_album_path GET /api/albums TeacherWeb.Api.AlbumController :index
api_album_path GET /api/albums/:id TeacherWeb.Api.AlbumController :show
Now that we have our routes, let’s create the album_controller.ex
to handle them. Since we included the “Api” prefix, we’ll first create an “api” directory. Then we’ll create our album_controller.ex
and define our controller module.
Now we need to define two actions to handle our two routes, so let’s create an “index” function, taking our connection, and we can ignore our params
and then we’ll want a “show” action.
Let’s implement the “index” action. We’ll want it to return a list of all our albums and to get all our albums we can use the Recordings.list_albums
function. Then let’s alias the Recordings
module so we can use it without the prefix.
Now the fastest way for us to render our JSON is to the use the Phoenix.Controller.json
function. Let’s include that here. It will take our connection, and then the data we want to use in the response.
lib/teacher_web/controllers/api/album_controller.ex
defmodule TeacherWeb.Api.AlbumController do
use TeacherWeb, :controller
alias Teacher.Recordings
def index(conn, _params) do
albums = Recordings.list_albums()
json(conn, %{data: albums})
end
def show(conn, params) do
end
end
Then we’ll go to the command line and start up our server.
$ mix phx.server
...
Ok, let’s head over to our http://localhost:4000/api/albums route. And we see we’re getting an error: “protocol Jason.Encoder not implemented” for our Album struct.
Looking at our error message, we see that we’re actually given a few options to resolve this. If we own the struct we could derive the implementation and specify what fields to use or we could encode all fields. If we don’t own the struct we can use the Protocol.derive
function.
We own the album struct so let’s go ahead and use the first suggestion. deriving the implementation and specifying which fields to use. We’ll open our album.ex
module and add derive Jason.Encoder
specifying the fields we want: “id” and “title”.
lib/teacher/recordings/album.ex
defmodule Teacher.Recordings.Album do
use Ecto.Schema
import Ecto.Changeset
@derive {Jason.Encoder, only: [:id, :title]}
...
end
Then if we go back to the browser and reload the page - great all our albums are being returned, with only the fields we specified. We were able to get our API working in hardly any time at all. While our current implementation works, since Phoenix is a MVC framework let’s see how we can leverage views to help render our data.
Let’s go back to our album.ex
module and remove our call to @derive
. Then let’s open our album_controller.ex
and instead of calling the json
function here, let’s now call the render
function, passing in our connection, the name of the template - let’s call this “index.json” - and then we’ll pass in any assigns, in this case our albums.
lib/teacher_web/controllers/api/album_controller.ex
...
def index(conn, _params) do
albums = Recordings.list_albums()
render(conn, "index.json", albums: albums)
end
...
Now that we’ve updated our controller, we’ll need to create our view. Just like we did for our controller, we’ll first create an “api” directory only now we’ll create it inside of “views”. Then we can create our album_view.ex
and define the view module.
We can define a render
function and we’ll pattern match on the string “index.json”, and also on the assigns so we can get our albums. Since our albums here are a list of album structs, Phoenix gives us the render_many
function to make it easy to render our collection. We’ll call render_many
passing in our albums
, then the view module, and finally a template that we’ll call “album.json”.
We’ll also need to define our “album.json” render function, which we can do by creating another render
function and pattern matching on the name - “album.json” - and then pattern matching to get our album. And inside this function we’ll return a map of the attributes we want to return in our API.
lib/teacher_web/views/api/album_view.ex
defmodule TeacherWeb.Api.AlbumView do
use TeacherWeb, :view
def render("index.json", %{albums: albums}) do
%{data: render_many(albums, TeacherWeb.Api.AlbumView, "album.json")}
end
def render("album.json", %{album: album}) do
%{id: album.id,
artist: album.artist,
title: album.title}
end
end
Our view should now be setup to handle our “index” action. Let’s go back to the browser and if we test our route again - great we see our data is being returned and we see our three fields are being displayed for each album.
Now that we have our “index” action working, let’s implement our show. We’ll go back to the album_controller.ex
and since we’ll need want to look up an album by its ID let’s pattern match to get the “id” from the params. Then we’ll use the Recordings.get_album!
function to get our album.
With our album we can call the render
function again. Only this time we’ll use “show.json” and we’ll pass in a single album as the assigns.
lib/teacher_web/controllers/api/album_controller.ex
...
def show(conn, %{"id" => id}) do
album = Recordings.get_album!(id)
render(conn, "show.json", album: album)
end
...
Then we’ll go to our album_view.ex
and we’ll implement our render
function to handle our new “show.json”, pattern matching on our single “album”. In the render function for our “index.json” we used render_many
to render a collection. Here we can use the render_one
function to render a single item, giving it our album
, the view module we want to use and since we just want to display the same fields we can use our same “album.json” render function.
lib/teacher_web/views/api/album_view.ex
...
def render("show.json", %{album: album}) do
%{data: render_one(album, TeacherWeb.Api.AlbumView, "album.json")}
end
...
With that let’s go back to the browser, and we see our index action listing our albums. Let’s go ahead and grab an album ID and then request the API “show” action. And great - the expected single album is returned. We’re getting the album’s artist and title, but one thing that’s missing is the album’s category.
Let’s open our album.ex
module. And we can see in our schema that our album has a belongs_to
relationship with a category. Let’s update our API to display album’s category too.
Now we could simply get our category name from our album and display that as a field on our Album, but what if we wanted to display more fields from our category? For our example let’s display the ID along with the category name.
Instead of filtering for the fields want here, let’s instead create a separate CategoryView
module to use. We’ll create a new file category_view.ex
and then inside it we’ll define our module.
Now let’s create a render
function and since this will render information about a single category, we’ll use the same pattern as we did earlier calling this “category.json”, and then we’ll pattern match to get our category. And inside the function let’s return a map with the fields we want to render: the category’s “id” and “name”.
lib/teacher_web/views/api/category_view.ex
defmodule TeacherWeb.Api.CategoryView do
use TeacherWeb, :view
def render("category.json", %{category: category}) do
%{id: category.id,
name: category.name}
end
end
Great, now that we have our CategoryView
setup, let’s go back to our album_view.ex
and in our render
for our “album.json” let’s add our category. Since an album belongs to a single “category” we’ll use the render_one
function, passing in the album’s category, the view module - in this case our CategoryView
, and finally the “category.json” template we defined.
lib/teacher_web/views/api/album_view.ex
...
def render("album.json", %{album: album}) do
%{id: album.id,
artist: album.artist,
title: album.title,
category: render_one(album.category, TeacherWeb.Api.CategoryView, "category.json")}
end
...
Now let’s test that our categories are working. We’ll go back to the browser and reload the page. And great - we see our category data is now being displayed along with our album. Let’s try our index route too. Every album is displaying the associated category.
David Tang
5 years agoAt 5:26 on line 8, where does the key “album” come from in that 2nd parameter?
Alekx
5 years agoHey David,
Above, on line 5, when we call
render_many
Phoenix infers the key to use from the module name.This function is a convenience that Phoenix provides, but behind the scenes it’s essentially doing this:
Serguei Cambour
5 years ago@derive
is commented?Alekx
5 years agoHey Serguei,
Thanks for the comment. You’re right - the format in this episode does not follow the JSON API spec. I do cover that spec using the JaSerializer library in episodes #83 and #84.
Regarding why that line is commented, in the episode the first way we return data is by deriving the implementation and specify that we want the
:id
and:title
fields returned. I then comment it out to show how we can use the view to render our data. Hope this helps!Cheers, Alekx
majirieyowel
2 years agoIn the API view the response structure for album.json is: %{ id: 1, artist: “Name”, title: “Title” }
But when you request the resource from the browser the positions of the items change to %{ artist: “Name”, id: 1, title: “Title” }
How can i make the items retain their original positions when requesting the resource from the browser?