Building an API Wrapper with Elixir and Tesla

Or how to make your Elixir application talk to a REST API in minutes, literally

Photo by Malcolm Lightbody on Unsplash

What is Elixir

If you don’t know that yet, I’m a bit surprised you are reading this article, but at the same time I’m kind of jealous, because learning about Elixir was one of the best moments in my professional career. After spending too much time with legacy code written in PHP or in Node.js’ callback hell, it was like a breath of fresh air: as if Ruby and Erlang suddenly got a child that inherited the syntactic beauty and simplicity of the former and the performance capabilities of the latter. Long story short, it was love at first sight. But instead of me telling you how wonderful Elixir is, just take a look at https://elixir-lang.org or https://phoenixframework.org, and I’m pretty sure you’ll love it too.

What is Tesla

I know what you’re thinking: why the hell would anyone create a library called Tesla if it’s not related to that car company? Even if it was in 2015? Turns out the name comes from the analogy with the Ruby library of a similar purpose, Faraday (assuming that it was named so after the scientist and not the Lost character). Anyway, name aside, Tesla is a wonderful library which lets you make your Elixir application talk to an external REST API literally in minutes. Shall we give it a try?

Let’s Pick a Target

Of course, almost every solid Web-platform has a REST API nowadays (yes, even Facebook, my dear GraphQL fans), so we could just go with another Twitter client, but it would be way more interesting to try out something new and underrepresented in the Elixir land. Hence, let’s assume we just picked something out of thin air and got this: https://indico.io. As they state on the site, it’s “the industry’s first practical approach for automating unstructured content processes”, which essentially means a platform that lets you analyze content (for sake of simplicity, let’s consider texts, mostly) using some AI and make human-like decisions based on the results. Isn’t it cool? Oh, and yeah, API docs are here: https://indico.io/docs.

It’s That Easy

For sake of simplicity we’ll assume here that you already have Elixir installed (otherwise https://elixir-lang.org/install.html has got you covered), and also created a new application using the following command:

mix new indico_io

Let’s start with putting some bare minimum inside lib/indico_io.ex:

defmodule IndicoIO do
@moduledoc """
Indico.io API wrapper
"""
  use Tesla
plug Tesla.Middleware.BaseUrl, "https://apiv2.indico.io"
plug Tesla.Middleware.Headers,
[{
"X-ApiKey",
Application.get_env(:indico_io, :indico_io_api_key)
}]
plug Tesla.Middleware.JSON
  def sentiment(data) do
{:ok, %Tesla.Env{:body => body}} = post(
"/sentiment",
%{:data => data}
)
    {:ok, body |> Jason.decode!}
end
end

In order to figure out what’s happening here, we’ll go over the most important pieces of the code.

First,

  use Tesla

lets us, well, use Tesla functions inside our module without specifying that they come from Tesla every time and also performs some initializations.

Next, we have a series of plug calls (which is also a macro from the Tesla library):

  plug Tesla.Middleware.BaseUrl, "https://apiv2.indico.io"
plug Tesla.Middleware.Headers,
[{
"X-ApiKey",
Application.get_env(:indico_io, :indico_io_api_key)
}]
plug Tesla.Middleware.JSON

What we do here is we set up our endpoint base url, authentication header to be sent with every call (requires some additional configuration, see below) and also tell Tesla that we’ll be using JSON as request / response body format.

Finally,

  def sentiment(data) do
{:ok, %Tesla.Env{:body => body}} = post(
"/sentiment",
%{:data => data}
)
    {:ok, body |> Jason.decode!}
end

is our API function, and it’s as simple as getting a JSON response from a POST request and decoding it.

However, if we try to compile our application now, it’s not going to work, simply because we need to perform a couple of extra steps to finalize our setup. The first one is to add Tesla (and it’s JSON dependency) to our mix.exs file:

defp deps do
[{:tesla, "~> 1.0.0"},
{:jason, ">= 1.0.0"}] # required by JSON middleware
end

Once it’s done, run

mix deps.get

to fetch the dependencies.

The final step is adding Indico.io API key to our config/config.exs:

config :indico_io, indico_io_api_key: "<Get your key for free from https://auth.indico.io/register>"

After that you should be all set. Try firing up Elixir REPL and test your sentiment function, which according to the docs should “quickly and efficiently determine if text is positive or negative”:

➜ iex -S mix
Erlang/OTP 20 [erts-9.3.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
==> jason
Compiling 8 files (.ex)
Generated jason app
==> mime
Compiling 2 files (.ex)
Generated mime app
==> tesla
Compiling 23 files (.ex)
Generated tesla app
==> indico_io
Compiling 1 file (.ex)
Generated indico_io app
Interactive Elixir (1.6.5) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> IndicoIo.sentiment "I feel good!"
{:ok, %{"results" => 0.9360338577753942}}
iex(2)> IndicoIo.sentiment "yikes!"
{:ok, %{"results" => 8.545276187403398e-4}}
iex(3)>

Beautiful, isn’t it? Note that Indico.io API also lets you process a series of data within a single request: all you have to do is to send a list of phrases as the parameter. Making it work in Elixir is amazingly simple. We just need to add another function clause with a guard above the main one:

  def sentiment(data) when is_list(data) do
{:ok, %Tesla.Env{:body => body}} = post(
"/sentiment/batch",
%{:data => data}
)
    {:ok, body |> Jason.decode!}
end

And a quick test gives us:

➜ iex -S mix
Erlang/OTP 20 [erts-9.3.1] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Interactive Elixir (1.6.5) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> IndicoIo.sentiment(["yes-yes-yes!", "no way!"])
{:ok, %{"results" => [0.9344446305911374, 0.005659668743777363]}}
iex(2)>

That’s it, really. Of course, this code only covers one API endpoint, but most of the others are absolutely similar. So similar, in fact, that you can use Elixir macros to generate the corresponding functions (and as much as discouraged the usage of macros generally is, this seems to be a rather valid use case). Hence, the following module will give you access to over a dozen API functions, both single-input and batch versions:

defmodule IndicoIO do
@moduledoc """
Indico.io API wrapper
"""
  use Tesla
plug Tesla.Middleware.BaseUrl, "https://apiv2.indico.io"
plug Tesla.Middleware.Headers, [{"X-ApiKey", Application.get_env(:indico_io, :indico_io_api_key)}]
plug Tesla.Middleware.JSON
  @endpoints [
sentiment: "Quickly and efficiently determine if text is positive or negative",
sentimenthq: "Highly accurate sentiment analysis but less performant than the standard Sentiment API",
texttags: "Determine the topics in the phrase or document",
language: "Automatically determine the language of a phrase or document",
political: "Gauge the political leanings of a phrase or document",
keywords: "Identify the important words within a document",
people: "Identify references to specific persons found in a document",
places: "Identify references to specific places found in a document",
organizations: "Identify references to specific organizations found in a document",
twitterengagement: "Predict audience engagement for a given tweet",
personality: "Predicts the personality traits of a text's author",
textfeatures: "Convert text into meaningful feature vectors",
emotion: "Predicts the emotion expressed by an author in a sample of text",
summarization: "Summarizes long documents by extracting important sentences"
]
  @endpoints |> Enum.each(fn {name, desc} ->
@doc """
#{desc}
    ## Examples
        iex> {:ok, result} = IndicoIO.#{name}("I feel good!")
iex> Map.has_key?(result, "results")
true
    """
def unquote(name)(data) when is_list(data) do
{:ok, %Tesla.Env{:body => body}} = post("/#{unquote(name)}/batch", %{:data => data})
      {:ok, body |> Jason.decode!}
end
    def unquote(name)(data) do
{:ok, %Tesla.Env{:body => body}} = post("/#{unquote(name)}", %{:data => data})
      {:ok, body |> Jason.decode!}
end
end)
end

Note, that this code also includes the documentation for your library and the tests. Isn’t Elixir just amazing? Check out the result at https://github.com/bredikhin/indico and https://hex.pm/packages/indico (and the automatically generated docs here: https://hexdocs.pm/indico).

Conclusion

In this article we used Tesla library to create an API wrapper for https://indico.io/, and it only took us about fifty lines of code to build a tested and documented library completely ready for distribution. If you wasn’t into Elixir before reading this, I really hope it at least got you interested, and we didn’t even touch the most powerful features of the language and the platform. So, if you feel like you got hooked, go check out https://elixir-lang.org.