Creating an SDK Pt. 3

Check out Part 1 here & Check out Part 2 here

We have OddsShark module that will abstract away all the particulars we need to deal with when querying the OddsShark API. Let’s implement that actual functionality of interacting with the OddsShark API. I’ve reposted the function chart we derived from last time, as we’ll now actually implement those functions since we have a common OddsShark module that will handle all the heavy HTTP-based lifting we need to do.

Endpoint Params Return Values
/upcoming/us/<league> <league> [%Matchup{}]
/ticker/<league> <league> [%Ticker{}]
/scores/<league>/<date> <league>, <date> [%Score{}]
/gamecenter/<league>/<id> <league>, <id> %Gamecenter{}
/play_by_play/<league> <league> [%Play{}]

Let’s just start with the first row. We’ll name the function matchups, since it will return a list of %Matchups. The one input parameter we’ll take is a string called league, which in the context of OddsShark can be mlb, nba, ncaab, nhl, and nfl (there may be a few more, but that’s a pretty decent list). Thinking about the internal process of this function call, it’ll go something like this:

build_endpoint ->
make_request ->
handle_response ->

We’ll make the request after building our endpoint (which we know will be /upcoming/us/&lt;league&gt;), and return an :error tuple if a status code other than 200 is returned. If a 200 is returned, we’ll return an :ok tuple with the response.

Create a oddsshark directory in lib/ - this will house most of our functionality, with each file corresponding to a response data shape. Within the new oddsshark directory, create a matchup.ex file. We’ll implement a simple get function to hit the endpoint we’ve defined and return the result.

The skeleton of our matchup.ex file is below:

defmodule OddsShark.Matchup do
  def get(league) do



I’ve added the get(league), since we know we need the league parameter. We also know that the endpoint we hit will be consistent, so let’s add @base_path "upcoming/us" underneath the defmodule OddsShark.Matchup do so that we can make clear which endpoint we’re hitting.

As we said before, we’ll need to build the endpoint. We know that we’ll always have one parameter, and that our endpoint is defined, so this will be pretty easy.

defp get_path(league) do
  Path.join(@base_path, league)

We’ll also need a function to make the request and handle the response. We didn’t add any response handling in part 2 within our OddsShark module, but it’ll probably something we’ll want in all of our individual files. Let’s switch over to oddsshark.ex and add a handle_response function.

The handle_response will take in a response to a request. These responses come in the format of:

{status_code, data}

Following the HTTP spec, a success status code is 200. A status code other than that, for all intents and purposes, indicates that an error occurred. We’ll want to return an error message if we do return an error. Since we don’t receive much information about errors from OddsShark (try passing in a league that doesn’t exist), we’ll just return a generic error message. The first handle_response function below is what we implemented last time - it matches responses that return a status_code of 200. The second implementation matches an :ok response (meaning that no unexpected errors occurred when making the request with HTTPosion) but without a 200 status code, which to us means an error occurred.

def handle_response({:ok, %{body: body, status_code: 200}}) do
  {:ok, process_response_body(body)}

def handle_response({:ok, %{}}) do
  {:error, "an error occurred"}

We can now add a second private function to our Matchup model, which will actually make the request and handle the response. The implementation is below - it uses the OddsShark module we created in the last part.

defp make_request(url) do
  |> OddsShark.get_request    

The /upcoming/us/<league> call is interesting in that it returns a data shape where the information we want is nested within the data key in the response Map. We’ll need to grab that data from the data key if the request is successful. Because we handle the successful and failed responses, we know we’ll receive either an :ok tuple or :error tuple, so we can implement a simple case statement that’s so common in Elixir applications.

def get(league) do
  case make_request(league) do
    {:ok, res} -> {:ok, Map.get(res, "data")}
    {:error, reason} -> {:error, reason}

This is really clear! We know if we receive a successful response, we need to grab the data key. Otherwise, just pass through the formatted error from the handle_response error handler we implemented in the OddsShark module. This makes it easy for us to implement better error message in the future, with one central place for us to handle the errors and format the error message.

We’ll go through a similar process for all the other endpoints we defined above. You can see the full project here. Thanks for following along!