Subscribe to access all episodes. View plans →

#82: JSON API with Phoenix 1.4

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.

© 2024 HEXMONSTER LLC