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.

How I Built an Elixir Wrapper for 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.

elixir-lang.github.com
Website for Elixir

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.

jrusso1020/alpaca_elixir
Alpaca API Elixir client wrapper. Contribute to jrusso1020/alpaca_elixir development by creating an account on GitHub.

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.

External API’s Should Allow for Free Dev Accounts
Recently I wanted to create a wrapper for a third party API. It wasn’t an API I used myself(at least not at the moment), but seeing as there was no wrapper for this language yet I thought I’d give it a go. Luckily this company provides a test account and sandbox environment for every account free of…

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.

lob/lob-elixir
Elixir Library for Lob API. Contribute to lob/lob-elixir development by creating an account on GitHub.

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.

jrusso1020/alpaca_elixir
Alpaca API Elixir client wrapper. Contribute to jrusso1020/alpaca_elixir development by creating an account 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/

All posts
Blog discussing software engineering, web development, technology, and life
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.