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 :)