How I Built an Elixir Wrapper for Alpaca API
James Russo, Software Engineer at Brex, writes about how he built an Elixir wrapper for the Alpaca API.
This is part of a series of guest posts from Alpaca community members. This week, we have James, a Software Engineer @brexHQ, writing about how he built an Elixir wrapper for the Alpaca API.
Why Built an Elixir Wrapper
Recently I switched jobs and started coding in Elixir professionally.
I feel proficient enough at work where most things are set up and running, it's relatively easy to develop and add new features. However, I wanted to get a better understanding of setting up an Elixir project and tooling from scratch. So I decided to start simple and try to write an Elixir wrapper for a 3rd party API.
I thought it would be a simple enough project to get up and running before jumping into a more involved project.
I had recently seen an article about Alpaca, a service that allows you to "trade with algorithms, connect with apps, build services — all with a commission-free stock trading API." So I took a look at their documentation and didn't see an Elixir wrapper yet. I then did a quick google search and saw one Elixir github project using it but it was years old and unfinished.
Seeing as there weren't any existing wrappers, I decided to give it a go. So I signed up for an account and started reading through their documentation. The culmination of this is "alpaca_elixir" the Elixir wrapper for the Alpaca API.
One amazing thing about Alpaca is that they actually offer a free sandbox environment for every account. With this sandbox account, you can issue API requests just as you would in real life but with fake money. I think this is fantastic, I can actually issue requests against an API without having to use real money. It costs me nothing, and could benefit countless developers in the future who want to try Alpaca out.
I wrote more about this on my personal blog, why it’s great and why more companies should do this if anyone is interested.
Building an Elixir API Wrapper
First thing I had to do was check out the Alpaca API and see what endpoints we needed to support which was relatively straightforward. They have all their documentation online and I would say in general it’s pretty concise and RESTful. The overall RESTfulness of their API made it relatively straightforward to develop against, and with the use of Elixir macros it was incredibly easy to write endpoints quickly.
The RESTfulness of most of Alpaca’s API presented me with an interesting solution since I was using Elixir.
Elixir offers the ability to write macros, or code that writes code. Looking through the Lob Elixir API I noticed they used a macro to define a ResourceBase module that allowed them to basically define the core RESTful operations once with a macro and then inject this code into individual modules and just change the endpoints that are being hit.
So I decided to take a similar approach based on the RESTfulness of Alpaca’s API. This means I could define the base RESTful CRUD operations once and then basically just define modules that `use`’s this module and have all the endpoints set up. I’ve included the full code at the bottom of this post, but you can also see it here on Github.
This means I actually spent most of my time writing tests and documentation instead of writing code. Once this `Resource` module was defined most of the endpoint modules were essentially one line of code to `use Alpaca.Resource, endpoint: "orders", exclude: [:update]`. There were some endpoints that didn’t really fit into the CRUD resource definition so in those cases I implemented them separately using the already predefined HTTP Client.
Because of the ease to add new endpoints, I was able to implement all of the Alpaca API V2 functionality in less than a month coding in my spare time after work and on weekends. And like I said a majority of my time was spent writing tests and documentation.
Speaking of documentation, this is a core part of an open source software project I believe and another way Elixir really shines. Elixir has built documentation into your development workflow by allowing you to easily define documentation for your modules and functions and then autogenerating it into HTML documents with embedded markdown. You can even write examples that are run as tests in your testing suite. This documentation is then auto deployed and hosted along with your hex package. You can find the alpaca_elixir documentation here https://hexdocs.pm/alpaca_elixir/AlpacaElixir.html.
One thing to note is that obviously the streaming API of Alpaca was not implementable using the the same HTTP client I defined for the rest of the HTTP endpoints. However, with the help of the WebSockex library I was able to define a stream module in Elixir that does the initial setup and authorization for you and allow developers to just define their own functions to handle incoming messages. You should also be able to add the `Alpaca.Stream` module to your supervision tree and run it in its own process which is a really cool plus of Elixir here. You can see this code here in the Github repo. (A bit of warning, this stream code is untested (at time of writing) and I would not recommend it in a production setting quite yet. However, I am hoping to get some feedback from users and fix any issues that may arise.)
Overall, the process of writing an Alpaca API Wrapper was pretty straightforward and simple because of the great documentation and RESTful practices followed by the Alpaca team and the choice to use Elixir. Also again I would just like to touch on the amazing development of the Alpaca team to offer a free fully functionality sandbox environment to all their users. This was an incredible choice and really allows them to foster a great developer community around their product, something more companies should do.
I look forward to my first users of my Elixir wrapper and please submit any issues on Github that come up! I will do my best to fix them in a timely manner, but am also always open to any open source contributors that would like to help out. You can contact me (James) on: https://boredhacking.com/
defmodule Alpaca.Resource do
@moduledoc """
This module uses a macro to allow us to easily create the base
HTTP methods for an Alpaca Resource. Most Alpaca requests have
a common set of methods to get a specific resource by ID, list
all resources, update the resource, delete all resources
and delete a resource by id. By using this macro we can easily
build out new API endpoints in a single line of code.
### Example
```
defmodule Alpaca.NewResource do
use Alpaca.Resource, endpoint: "new_resources"
end
```
We that single line of code we will now have a `list/0`, `list/1`,
`get/1`, `get/2`, `create/1`, `edit/2`, `delete_all/0`, and `delete/1`
functions for a given resource.
You can also exclude functions from being created by passing them as a list
of atoms with the `:exclude` keyword in the resource definition.
### Example
```
defmodule Alpaca.NewResource do
use Alpaca.Resource, endpoint: "new_resources", exclude: [:delete_all, :delete]
end
```
This definition will not create a `delete_all/0` or `delete/1` endpoint for the
new resource you have defined.
"""
defmacro __using__(options) do
endpoint = Keyword.fetch!(options, :endpoint)
exclude = Keyword.get(options, :exclude, [])
opts = Keyword.get(options, :opts, [])
quote do
alias Alpaca.Client
unless :list in unquote(exclude) do
@doc """
A function to list all resources from the Alpaca API
"""
@spec list(map()) :: {:ok, [map()]} | {:error, map()}
def list(params \\ %{}) do
Client.get(base_url(), params, unquote(opts))
end
end
unless :get in unquote(exclude) do
@doc """
A function to get a singlular resource from the Alpaca API
"""
@spec get(String.t(), map()) :: {:ok, map()} | {:error, map()}
def get(id, params \\ %{}) do
Client.get(resource_url(id), params, unquote(opts))
end
end
unless :create in unquote(exclude) do
@doc """
A function to create a new resource from the Alpaca API
"""
@spec create(map()) :: {:ok, map()} | {:error, map()}
def create(params) do
Client.post(base_url(), params, unquote(opts))
end
end
unless :edit in unquote(exclude) do
@doc """
A function to edit an existing resource using the Alpaca API
"""
@spec edit(String.t(), map()) :: {:ok, map()} | {:error, map()}
def edit(id, params) do
Client.patch(resource_url(id), params, unquote(opts))
end
end
unless :update in unquote(exclude) do
@doc """
A function to update an existing resource using the Alpaca API
"""
@spec update(String.t(), map()) :: {:ok, map()} | {:error, map()}
def update(id, params) do
Client.put(resource_url(id), params, unquote(opts))
end
end
unless :delete_all in unquote(exclude) do
@doc """
A function to delete all resources of a given type using the Alpaca API
"""
@spec delete_all() :: {:ok, [map()]} | {:error, map()}
def delete_all() do
with {:ok, response_body} <- Client.delete(base_url(), unquote(opts)) do
{:ok,
Enum.map(response_body, fn item ->
%{
status: item.status,
id: item[:id] || item[:symbol],
resource: item.body
}
end)}
end
end
end
unless :delete in unquote(exclude) do
@doc """
A function to delete a singular resource of a given type using the Alpaca API
"""
@spec(delete(String.t()) :: :ok, {:error, map()})
def delete(id) do
with :ok <- Client.delete(resource_url(id), unquote(opts)) do
:ok
end
end
end
@spec base_url :: String.t()
defp base_url() do
version = Keyword.get(unquote(opts), :version, "v2")
"/#{version}/#{unquote(endpoint)}"
end
@spec resource_url(String.t()) :: String.t()
defp resource_url(resource_id), do: "#{base_url()}/#{resource_id}"
end
end
end
Technology and services are offered by AlpacaDB, Inc. Brokerage services are provided by Alpaca Securities LLC, member FINRA/SIPC. Alpaca Securities LLC is a wholly-owned subsidiary of AlpacaDB, Inc.