Phoenix JSON API: Rendering many

This lesson continues from https://alchemist.camp/episodes/phx-json-api-start, where we set up a very simple API endpoint to return a random number from 1 to 6.

Rendering multiple items

A common need in APIs is to return a collection of similar items. They could be tweets or blog posts or any kind of structure. We'll stick with die rolls for simplicity and add an endpoint that returns some number of die roll results. Open up router.ex and add a new route under the one we created for single die rolls:

  scope "/api", FirehoseWeb do
    pipe_through :api

    get "/roll", RollController, :index
    get "/roll/:num_dice", RollController, :show
  end

Notice the atom, :num_dice inside the path. This means that the router will match any GET request to /api/roll/:num_dice with anything appended to the end of it and then call the show function in the roll controller and pass in whatever was at the end of the URL as the parameter :num_dice. For example, a request to /api/roll/7 would call RollController.show/2 with the connection struct and the key "num_dice" would be set to "7" in the params.

To handle this call, we just need to write a show function and match "num_dice" out of the params.

  def show(conn, %{"num_dice" => num_dice}) do
    rolls = case Integer.parse(num_dice) do
      {num, _} -> for x <- 1..num, do: %{die: x, value: :rand.uniform(6)}
      _ -> :error
    end

    render(conn, "show.json", rolls: rolls)
  end

The logic in this function uses Integer.parse/1 to ensure that num_dice captured from the end of the URL is in fact an integer and then renders a list of structs, which include a roll value and an index of the die. E.g.,

[
  %{die: 1, value: 3},
  %{die: 2, value: 6},
  %{die: 3, value: 6},
  %{die: 4, value: 2}
]

Refactoring the view

What we want is a render function head that matches show.json in its first argument and then returns that same data converted into JSON:

{
  "data":[
    {"die":1,"value":3},
    {"die":2,"value":6},
    {"die":3,"value":6},
    {"die":4,"value":2}
  ]
}

Since this is very similar to what we already have for handling a single die roll, it makes sense to share the same logic across both the endpoints. We can do that by defining a "roll.json" which represents a single roll as %{die: die, value: num} or just as %{value: num} if no die number is supplied. Then we can use this helper by calling render_one or render_many from the render functions for the API endpoints:

defmodule FirehoseWeb.RollView do
  use FirehoseWeb, :view

  def render("index.json", %{roll: roll}) do
    %{data: render_one(roll, __MODULE__, "roll.json")}
  end

  def render("roll.json", %{roll: %{die: die, value: num}}) when is_integer(num) do
    %{die: die, value: num}
  end

  def render("roll.json", %{roll: %{value: num}}) when is_integer(num) do
    %{value: num}
  end

  def render("roll.json", _), do: %{status: "error"}

  def render("show.json", %{rolls: rolls}) do
   %{data: render_many(rolls, __MODULE__, "roll.json")}
  end
end

Both functions take a variable with the data, the name of the view module to render it (__MODULE__ which just means the current module, i.e., RollView), and the name of the template ("roll.json"). In the case of render_many, the rolls variable needs to contain a list of roll structs that can be handled by the "roll.json" function head.

Done?

Make sure all the changes are saved, start the server if it's not already running and go to localhost:4000/api/roll/3 to see the result of three die rolls.

There is one small piece to do though. In the process of writing this, we slightly changed the input passed to the view for single die rolls. See if you can update the index function in the controller to work with the new view. The solution fix is at the end of the screencast embedded on this page if you get stuck.

Back to index

No Comments