Building an Elixir Umbrella App with Phoenix and React: Part II

Building a YouTube API Client with Elixir

In Part I of this series, we learned about umbrella apps, and designed the archicture of our brand new umbrella app. Our umbrella app is responsible for communicating with YouTube regarding the state of song deliveries. It will have two child apps. One child app, Deliveries will be an Elixir application that contains a YouTube API client.

In this post, we'll build out this child app and its API client.

Building the Deliveries Child App

We'll cd into the apps/ directory of our umbrella app and generate our project:

$ cd apps/
$ mix new deliveries

We'll build another underneath Deliveries to house our YouTube API communication code.

Building a YouTube Client

We'll create a YouTube.Client module to underly our communications with the YouTube API.

Our client should be fairly generic. It will:

  • Construct the authorization headers, with help of the Goth library to fetch our authorization token.
  • Use HTTPoison to make the API request

Let's configure our dependencies––Goth and HTTPoison––before we write the code that relies on them.

Configuring Dependencies

In our child app's mix.exs file, we'll add our dependencies, and start them up when the applications starts:

apps/deliveries/mix.exs
...
def application do
  [extra_applications: [:logger, :httpoison, :goth]]
end

defp deps do
  [
    {:goth, "~> 0.4.0"},
    {:httpoison, "~> 0.11.1"}
  ]
end

In order to use Goth, we'll have to pass in our Google Cloud Services credentials:

# apps/deliveries/config/config.exs
use Mix.Config
config :goth,
  json: System.get_env("GOOGLE_APPLICATION_CREDENTIALS") |> File.read! |> File.read!

I've stored my GSC credentials in a file, and set an environment variable, GOOGLE_APPLICATION_CREDENTIALS, equal to the path to that file

Okay, we're ready to write our client.

YouTube Client Module

We'll define our client, Deliveries.YouTube.Client in apps/deliveries/lib/youtube/client.ex

defmodule Deliveries.YouTube.Client do

  @auth_url "https://www.googleapis.com/auth/youtubepartner"

  def headers do
    {:ok, token} = Goth.Token.for_scope(@auth_url)
    ["Authorization": "Bearer #{token.token}"]
  end

  def make_request(url, method) do
    apply(HTTPoison, method, [url, headers])
  end

  def make_request(url, method, body) do
    apply(HTTPoison, method, [url, body, headers])
  end
end

Our client is light-weight and dynamic. It builds the authorization headers using Goth and our @auth_url module attribute, which we're treating like a constant.

It implements functions make_request/2 and make_request/3, which use Erlang's apply function to dynamically invoke HTTPoison with or without a request body.

Our client can be used by any future modules to talk to the YouTube API.

Now we're ready to build another module, Deliveries.YouTube.Status which will wrap this client and make the "ISRCs status check" API request.

Using our Client

Our Deliveries.YouTube.Status module is responsible for using the client to make the appropriate API request and handling the response to that request.

First, let's think about the API of our module. Eventually, our module will be called within our other child app, the Phoenix app (coming soon!). That app will take in a list of song ISRCs and expect to receive back confirmation of the presence of these ISRCs in YouTube's system. Our Phoenix app will call on the Status module like this:

isrcs = ["TCblahblah", "TCblahblah2"]
Deliveries.YouTube.Status.status_for(isrcs)

Our Status module will need to be able to take these ISRCs, construct the URL to pass to the client, capture the response from the client and handle it appropriately.

First, let's set the YouTube API status endpoint as a module attribute of our Status module:

defmodule Deliveries.YouTube.Status do
  alias Deliveries.YouTube.Client, as: Client
  @status_url "https://www.googleapis.com/youtube/partner/v1/assetSearch?type=sound_recording&ownershipRestriction=none"
end

Next, we'll define our status_for/1 function.

def status_for(isrcs) when is_list(isrcs) do
  case do_status_for(isrcs) do
  {:ok, response} ->
    case decode_response(response.body) do
      %{"error" => error_messages} ->
        {:error, decode_response(response.body)}
      _ ->
        {:ok, decode_response(response.body)}
    end
  {:error, response} ->
    {:error, decode_response(response.body)}
  end
end

defp do_status_for(isrcs) do
  @status_url <> "&isrcs=" <> Enum.join(isrcs, "%2C%20")
  |> Client.make_request(:get)
end

defp decode_response(body) do
  {:ok, body} = Poison.decode(body)
  body
end

Let's break this down a bit.

status_for/1 relies on a helper function, do_status_for/1. This helper function builds the URL and pipes it to a call to Client.make_request.

Then, our status_for/1 function uses a case statement to pattern match for a success or error response from the client, eventually returning a tuple:

  • For a successful response
{:ok, parsed_response_body}
  • For a failed response or a successful response describing an error message from YouTube
{:error, parsed_response_body}

Now that our Deliveries.YouTube module is built out, we're ready to set up our second child app, the Phoenix application. Join me for Part III to see how it works! Remember to subscribe below to get notified when Part III goes live :)

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus