Prev Next

Here’s the test we added to the handler_test.exs file.

test "GET /api/bears" do
  request = """
  GET /api/bears HTTP/1.1\r
  Host: example.com\r
  User-Agent: ExampleBrowser/1.0\r
  Accept: */*\r
  \r
  """
 
  response = handle(request)
 
  expected_response = """
  HTTP/1.1 200 OK\r
  Content-Type: application/json\r
  Content-Length: 605\r
  \r
  [{"type":"Brown","name":"Teddy","id":1,"hibernating":true},
   {"type":"Black","name":"Smokey","id":2,"hibernating":false},
   {"type":"Brown","name":"Paddington","id":3,"hibernating":false},
   {"type":"Grizzly","name":"Scarface","id":4,"hibernating":true},
   {"type":"Polar","name":"Snow","id":5,"hibernating":false},
   {"type":"Grizzly","name":"Brutus","id":6,"hibernating":false},
   {"type":"Black","name":"Rosie","id":7,"hibernating":true},
   {"type":"Panda","name":"Roscoe","id":8,"hibernating":false},
   {"type":"Polar","name":"Iceman","id":9,"hibernating":true},
   {"type":"Grizzly","name":"Kenai","id":10,"hibernating":false}]
  """
 
  assert remove_whitespace(response) == remove_whitespace(expected_response)
end

Rendering JSON in Elixir

Silencing Poison Warnings

To silence any warnings related to the poison library, update the version to 5.0. First, change the version in the mix.exs file:

defp deps do
  [{:poison, "~> 5.0"}]
end

Then install the dependencies:

mix deps.get

Organizing Code

You can organize your Elixir project code however you like under the lib directory. For example, you could create a lib/controllers directory for all controller files.

Tip: Dependency Version Format

Here’s how to specify multiple dependencies in mix.exs. For example, to add both poison and faker libraries:

defp deps do
  [{:poison, "~> 5.0"}, {:faker, "~> 0.8.0"}]
end

Each tuple in the list consists of a package name (an atom) and the version string.

So how do you remember what the version format means? If you’re ever in doubt, type h Version in an iex session to see a list of examples and explanations.

Exercise: Support Multiple Response Headers

In the video, we supported different content types by adding the resp_content_type field to the Conv struct. Instead of storing just one header, you can store response headers in a map like so:

resp_headers: %{"Content-Type" => "text/html"}

Now update the Handler.format_response function to use the map’s "Content-Type" value:

Content-Type: #{conv.resp_headers["Content-Type"]}\r

Setting Response Headers

Define a helper function, put_resp_content_type/2, that updates the content type:

def put_resp_content_type(conv, content_type) do
  headers = Map.put(conv.resp_headers, "Content-Type", content_type)
  %{ conv | resp_headers: headers }
end

You can call this helper in the controller like so:

def index(conv) do
  json = Servy.Wildthings.list_bears() |> Poison.encode!
  conv = put_resp_content_type(conv, "application/json")
  %{ conv | status: 200, resp_body: json }
end

Exercise: Handle POSTed JSON Data

To parse JSON in a POST request, use Poison.Parser.parse!. Here’s an example:

iex> json = ~s({"name": "Breezly", "type": "Polar"})
"{"name": "Breezly", "type": "Polar"}"
 
iex> Poison.Parser.parse!(json, %{})
%{"name" => "Breezly", "type" => "Polar"}

Write the code in your API controller to handle the incoming POST request and parse the JSON data. Define the action:

def create(conv) do
  params = Poison.Parser.parse!(conv.resp_body, %{})
  %{ conv | status: 201, resp_body: "Created a #{params["type"]} bear named #{params["name"]}!" }
end

Exercise: Read Bears from a JSON File

If you’d like more practice, move the bear data to a JSON file and use Poison.decode! to load it:

Poison.decode!(json, as: %{"bears" => [%Bear{}]})

Save the bear data in a JSON file, read it in your Wildthings module, and decode it into Bear structs.

Exercise: Serve Markdown Pages

You can add support for Markdown pages using a library like earmark. First, add the dependency:

defp deps do
  [{:poison, "~> 3.1"}, {:earmark, "~> 1.4"}]
end

Download the dependency:

mix deps.get

Next, create a route that matches requests for pages, reads the corresponding Markdown file, and converts it to HTML:

def route(%Conv{method: "GET", path: "/pages/" <> name} = conv) do
  @pages_path
  |> Path.join("#{name}.md")
  |> File.read
  |> handle_file(conv)
  |> markdown_to_html
end
 
def markdown_to_html(%Conv{status: 200} = conv) do
  %{ conv | resp_body: Earmark.as_html!(conv.resp_body) }
end
 
def markdown_to_html(%Conv{} = conv), do: conv

Code So Far

The code for this section is in the external-library directory found within the video-code directory of the code bundle

Prev Next