Writing API Wrappers with Elixir
Recently, I built an API wrapper for Mandrill. Let's walk through the steps I used to create it and see if this process might be able to help you in your next project.
Installing Elixir
If your preferred development environment does not have Elixir installed, you'll need to install it in order to continue on with the walk through. Head over to the Elixir getting started page, and follow their steps under section 1.1.
If you've never messed with Elixir, read the rest of the Elixir getting started page after installing Elixir. It's ok. I'll wait.
Ready? Let's go.
Setting up our project
Developers using Elixir use mix
for building, running, and testing their applications. What is mix
, you ask? Well, from its intro page:
Mix is a build tool that provides tasks for creating, compiling, testing (and soon releasing) Elixir projects. Mix is inspired by the Leiningen build tool for Clojure and was written by one of its contributors.
With mix, creating our project is as simple as:
$ mix new api_wrapper --sup
where api_wrapper
is the name of our project. I'm wanting to build a wrapper for Mandrill's API, so I'll be using mandrillex
for my project name. If you want a reliable transactional email provider or just want to follow along, check out their features page and signup. Psst: it's free up to 10,000 emails per month.
OTP application
Opening lib/mandrillex.ex
, we see mix
has set up a project for us that implements the bare necessities for an OTP application, allowing our wrapper to be included in other projects easier by following the OTP design principles.
defmodule Mandrillex do
use Application.Behaviour
# See http://elixir-lang.org/docs/stable/Application.Behaviour.html
# for more information on OTP Applications
def start(_type, _args) do
Mandrillex.Supervisor.start_link
end
end
We're going to leverage HTTPoison for making requests to our API. Lucky for us, HTTPoison exposes a HTTPoison.Base
module that we can embed into our module with the use
directive.
...
use Application.Behaviour
use HTTPoison.Base
...
Now when we want to use our module elsewhere, we can simply make a call to Mandrillex.start
to start the OTP application and listen for calls we want to send to the API.
We're also going to define process_url/1
and process_response_body/1
to make our lives easier when using HTTPoison
.
...
def process_url(endpoint) do
"https://mandrillapp.com/api/1.0/" <> endpoint <> ".json"
end
def process_response_body(body) do
JSEX.decode!(body, [{:labels, :atom}])
end
...
As you might be able to tell, process_url/1
allows us to shorten our urls from https://mandrillapp.com/api/1.0/users/ping.json
to users/ping
, and process_response_body/1
processes the response we receive automatically before it is returned to us.
Building out our wrapper
When compiling, Elixir will look for source files in our lib
directory, as long as the file extensions are correct (standard is *.ex
for files meant for compilation), and create the corresponding BEAM bytecode files. BEAM, the Erlang VM, is what handles the processes, fault-tolerance, applications, etc. for Erlang, Elixir, and other BEAM-based languages.
Most developers match up module names with directories, e.g. store Mandrillex
in lib
and Mandrillex.Users
in lib/mandrillex
. We're going to follow suit and place our other module files in lib/mandrillex
.
Building out all parts of this wrapper would get fairly monotonous, so instead, we'll focus on one endpoint, messages/send
.
In lib/mandrillex/messages.ex
, we're going to define our module and the function responsible for handling the messages/send
endpoint.
defmodule Mandrillex.Messages do
def send(key, message, async, ip_pool, send_at) do
params = [
key: key,
message: message,
async: async,
ip_pool: ip_pool,
send_at: send_at
]
Mandrillex.post("messages/send", JSEX.encode! params).body
end
end
Now, we can send calls to that endpoint with this method.
$ iex -S mix
Erlang R16B02 (erts-5.10.3) [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Interactive Elixir (0.11.3-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex> Mandrillex.start
:ok
iex> Mandrillex.Messages.send("your_api_key", [text: "testing", subject: "test subject", from_email: "sending email", from_name: "sending name", to: [[email: "recipient email", name: "recipient name", type: "to"]]], true, nil, nil)
[[email: "recipient email", status: "sent",
_id: "cb03be26672147dc9503ce2f90806492", reject_reason: nil]]
Awesomesauce! We just received a successful response from the API using our newly-developed module.
Streamlining our wrapper
I don't know about you, but since Mandrill's API calls alway need an API key passed, I would hate to have that as a parameter for all of the endpoints. Let's move that into the Mandrillex
module:
...
def key do
System.get_env("MANDRILL_KEY")
end
...
and change Mandrillex.Messages.send
to suit:
...
def send(message, async, ip_pool, send_at) do
params = [
key: Mandrill.key,
message: message,
async: async,
ip_pool: ip_pool,
send_at: send_at
]
Mandrillex.post("messages/send", JSEX.encode! params).body
end
...
When starting our iex
session, our expressions are only slightly different.
$ MANDRILL_KEY=your_api_key iex -S mix
Erlang R16B02 (erts-5.10.3) [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Interactive Elixir (0.11.3-dev) - press Ctrl+C to exit (type h() ENTER for help)
iex> Mandrillex.start
:ok
iex> Mandrillex.Messages.send([text: "testing", subject: "test subject", from_email: "sending email", from_name: "sending name", to: [[email: "recipient email", name: "recipient name", type: "to"]]], true, nil, nil)
[[email: "recipient email", status: "sent",
_id: "cb03be26672147dc9503ce2f90806492", reject_reason: nil]]
Of course, this is only one way of introducing an environment variable. Be sure to choose the method that makes the most sense for your implementation.
Moving on, I think it'd be nice to have a function that makes the request for me, since I'm always making POST
requests to Mandrill's API and always JSON-encoding the request body.
In the Mandrillex
module, we'll add:
...
def request(endpoint, body) do
Mandrillex.post(endpoint, JSEX.encode! body).body
end
...
and update Mandrillex.Messages
once more:
...
def send(message, async, ip_pool, send_at) do
params = [
key: Mandrill.key,
message: message,
async: async,
ip_pool: ip_pool,
send_at: send_at
]
Mandrillex.request("messages/send", params)
end
...
By doing this, we are able to change our code in one place instead of many places if we were to ever change how JSON is encoded, say if we switched from jsex
to exjson
.
If we were making more than POST
requests, we could add a method
parameter to Mandrillex.request
and add other definitions with guard clauses similar to:
...
def request(method, endpoint, body) when method == :get do
Mandrillex.get(endpoint, JSEX.encode! body).body
end
def request(method, endpoint, body) when method == :post do
Mandrillex.post(endpoint, JSEX.encode! body).body
end
...
and call it via Mandrillex.request(:post, "messages/send", params)
in Mandrillex.Messages.send
. Alternately, we could favor a more Erlang-ish approach with pattern matching:
...
def request({:get, endpoint, body}) do
Mandrillex.get(endpoint, JSEX.encode! body).body
end
def request({:post, endpoint, body}) do
Mandrillex.post(endpoint, JSEX.encode! body).body
end
...
and call it by wrapping the previous call's arguments as a tuple, i.e. Mandrillex.request({:post, "messages/send", params})
.
Wrapping up
We only have one endpoint covered, but our working function that allows us to make our call in a simple manner can be duplicated to handle all the other endpoints in the Mandrill API.
Now it's your turn to finish up, or if you're the impatient type, head over to slogsdon/mandrillex on GitHub to look at the current version of mandrillex
that covers the rest of the endpoints.