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.
No Comments