Introducing Tesla — the flexible HTTP client for Elixir

After a year and a half year since the first commit it is time to finally write something about tesla — the flexible HTTP client library for Elixir.

The story behind it is dead simple - coming from ruby world I was missing the fantastic faraday equivalent, so there was no other option than filling the gap. (You probably figured out origins of the name by now.)

Why another HTTP client library?

There are already some great elixir HTTP clients like httpotion or httpoison. Each of them is based on different erlang library, ibrowse or hackney. They both allow writing custom API clients providing specific callback to manipulate requests and responses. Having said that, the most important missing trait for me was the lack of support for composing API clients from ready-made blocks — middlewares. Having a rich, well-tested toolset right at hand is invaluable. Since besides middleware tesla also supports multiple different adapters we (the elixir community) can implement, share and reuse common patterns without worrying about underlying low-level details.

GET /HELLO-WORLD

You can use tesla directly like this:

Tesla.get("https://github.com/search", query: [q: "tesla"])

Let’s build a GitHub API client

Take a look at the following code snippet:

defmodule GitHub do
  use Tesla

  plug Tesla.Middleware.BaseUrl, "https://api.github.com"
  plug Tesla.Middleware.Headers, %{"User-Agent" => "tesla"}
  plug Tesla.Middleware.JSON

  def user_repos(login) do
    get("/user/" <> login <> "/repos")
  end
end

As you can probably already guess this GitHub API client will hit https://api.github.com with “tesla” User-Agent header. The request body will be encoded as JSON and response body will be decoded as JSON (if given supported Content-Type).

Now you can use this client as follows to get list of repos for a user:

GitHub.user_repos("teamon")

Dynamic API clients

I’m sure you are thinking about dynamic properties like authorization that you will need to pass around at function arguments. Worry not, tesla got you covered:

defmodule GitHub do
  use Tesla.Builder

  plug Tesla.Middleware.BaseUrl, "https://api.github.com"
  plug Tesla.Middleware.Headers, %{"User-Agent" => "Tesla"}
  plug Tesla.Middleware.JSON

  def new(token) do
    Tesla.build_client([
      {Tesla.Middleware.Headers, %{"Authorization" => token}}
    ])
  end

  def user_repos(client, login) do
    get(client, "/user/" <> login <> "/repos")
  end
end

Tesla.build_client/1 allows you to create a local client by dynamically inserting middleware (with arguments). Using that client is then as simple as this:

client = GitHub.new(my_token)
repos = client |> GitHub.user_repos("teamon")

How about middleware?

Every middleware is either a local function/3 or a module with function call/3. It takes Tesla.Env struct with request parameters, the next middlewares and some options.

defmodule MyMiddleware do
  def call(env, next, options // []) do
    env
    |> do_something_with_request
    |> Tesla.run(next)
    |> do_something_with_response
  end
end

You can choose to do whatever you want. See built-in middlewares for some examples.

There is more

At the moment of writing tesla supports three different erlang HTTP adapters: hackney, ibrowse and the built-in httpc. It has some basic middlewares for JSON, logging etc. If the adapter supports it you can pass a Stream as a request body.

I encourage you to play with it, write some middleware and submit a pull request — let’s build the best HTTP toolkit one can imagine!


Looking for comments section?

Send me an email instead to teamon@me.com