Composable APIs with Elixir pipes
Wednesday, 7 September 2016 · 7 min read · elixirA rockstar developer, climbing out of the mess that is your library.
Taking lessons from Plug, Ecto, and Swoosh let’s examine how these libraries create composable and intuitive APIs like so:
# Plug
conn
|> put_resp_content_type("text/plain")
|> send_resp(200, "Hello world")
# Ecto
Weather
|> where(city: "Kraków")
|> order_by(:temp_lo)
|> limit(10)
|> Repo.all
# Swoosh
new
|> to({user.name, user.email})
|> from({"Dr B Banner", "hulk.smash@example.com"})
|> subject("Hello, Avengers!")
|> html_body("<h1>Hello #{user.name}</h1>")
|> text_body("Hello #{user.name}\n")
Background
Because Elixir is immutable, libraries often feature some kind of model object (usually a struct
) that gets passed around throughout the library. Examples include Plug.Conn
and Ecto.Query
.
These model objects are used in methods that look like this:
def do_something(model_object, param1, param2) do
# Some computation
%{ model_object | new_attribute: "new value"}
end
For example, the Plug.Conn
model struct
has the following fields:
defstruct adapter: {Plug.Conn, nil},
assigns: %{},
before_send: [],
body_params: %Unfetched{aspect: :body_params},
cookies: %Unfetched{aspect: :cookies},
halted: false,
host: "www.example.com",
method: "GET",
owner: nil,
params: %Unfetched{aspect: :params},
path_info: [],
port: 0,
private: %{},
query_params: %Unfetched{aspect: :query_params},
query_string: "",
peer: nil,
remote_ip: nil,
req_cookies: %Unfetched{aspect: :cookies},
req_headers: [],
request_path: "",
resp_body: nil,
resp_cookies: %{},
resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}],
scheme: :http,
script_name: [],
secret_key_base: nil,
state: :unset,
status: nil
Wow! That’s a lot of fields! How on earth can a developer interact with an object of this complexity?
Naturally, we don’t expect anyone to be able (or want) to construct a valid payload of this caliber. Instead, as library authors we can provide a set of intuitive interfaces to interact with this gigantic struct
.
By providing a set of methods to interact with the underlying struct
, libraries ensure that each
update made by the user obeys contracts and passes validations that need to be enforced.
Example
Let’s look at one such method. Below is Plug
’s put_resp_content_type
:
def put_resp_content_type(conn, content_type, nil) when is_binary(content_type) do
conn |> put_resp_header("content-type", content_type)
end
def put_resp_header(%Conn{adapter: adapter, resp_headers: headers} = conn, key, value) when
is_binary(key) and is_binary(value) do
validate_header_key!(adapter, key)
validate_header_value!(value)
%{conn | resp_headers: List.keystore(headers, key, 0, {key, value})}
end
For your reference, here’s how put_resp_content_type
is used:
# Plug
conn
|> put_resp_content_type("text/plain")
|> send_resp(200, "Hello world")
Here’s a simplified version of put_resp_content_type
that’s a bit easier on the eyes:
def put_resp_content_type(conn, content_type) do
validate_content_type!(content_type)
%{ conn | resp_headers: List.keystore(headers, "content_type", 0, {"content_type", content_type})}
end
In essence, methods like put_resp_content_type
and order_by
does the following things:
- First, it validates the user
input
, - transforms the user
input
to follow some desired structure or formatting, - inserts the transformed
input
into thestruct
, and finally - returns the new, updated
struct
All of the above steps can occur without users needing to know the nitty-gritty implementation details beyond the method’s public signature.
In addition, by defining multiple methods you can create composable functions that is more idiomatic Elixir, due the prevalence of the pipe operator in the language.
# You'll see this pattern a lot in Elixir.
Weather
|> where(city: "Kraków")
|> order_by(:temp_lo)
|> limit(10)
|> Repo.all
In Closing
Many Elixir libraries such as Plug
and Ecto
provide functions that act as their public API, shielding users from the complexity underneath. This is a much better approach than having users build the model struct themselves (and inevitably screw up!)
📬 Get updates straight to your inbox.
Subscribe to my newsletter so you don't miss new content.