Minimal Elixir JSON RESTful API

How to expose a JSON endpoint in Elixir without any framework?

Kamil Lelonek
Kamil Lelonek  - Software Engineer

--

The pains of a new language

Lots of Elixir developers come from the Ruby world. It is a very mature environment when it comes to the available community libraries and frameworks. And this is what I sometimes miss in Elixir. When I want to integrate a 3rd-party service, the outcome might be as follows:

  1. there’s an official well-supported library (very rare)
  2. there’s an official but an outdated or a buggy library (happens sometimes)
  3. there’s a well-supported community library (every once in a while)
  4. there’s a community library but not maintained anymore(very common)
  5. there are multiple libraries, each written for their author’s needs and with missing features (the most popular)
  6. there’s my own library combining all the best from the above… (too often)

Simple JSON API in Elixir

You might be surprised, but Ruby is not always on Rails. It doesn’t have to be related to the web either. Although, in this particular case, let’s talk about the web exactly.

When it comes to exposing a single RESTful endpoint, we usually have a plenty of choices:

These are just the examples I’ve used personally. My colleagues are happy Sinatra users and they have tried Hanami before as well. I can pick whatever I need and like, even depending on my current mood.

However, when I switched to Elixir, it turned out that the choices are limited. Even though there are a few alternative “frameworks” (which, for the obvious reasons, I won’t mention here), they are almost unusable!

I spent the entire day playing with every library ever mentioned on the Internet. I tried to deploy a simple HTTP2 server to Heroku acting as a Slack bot but, at the end of the day, I surrendered. Literally, nothing I found was able to cover my basic requirements.

Phoenix is not always the solution

Phoenix is my favorite web framework ever, I just don’t always need it. I hesitated to use it because I wanted to avoid pulling the entire framework for just a single endpoint, no matter how easy it would be.

I couldn’t use any library either, because, as I mentioned, none of what I found was a fit for my needs (i.e. basic routing with JSON handling), and was convenient enough for an easy and fast deployment on Heroku. “Let’s go one step backward” — I thought.

Mind you, Phoenix itself is built on top of something, isn’t it?

Plug & Cowboy for the rescue

If we want to implement a truly minimal Ruby server, we would just use rack which is a modular Ruby webserver interface.

Fortunately, in Elixir we can use the similar thing. Here, we will leverage the following elements:

  • cowboy — a small and fast HTTP server for Erlang/OTP providing a complete HTTP stack and routing capabilities optimized for low latency and low memory usage
  • plug — connection adapters for different web servers in the Erlang VM and connection is a direct interface to the underlying web server
  • poison — just a JSON library for Elixir

Implementation

I want to implement components like Endpoint, Router, and JSON Parser. Then, I’d like to deploy that on Heroku and be able to handle incoming requests to the exposed endpoint. Have I look at how it can be achieved.

Application

Make sure your Elixir project is the supervised one. To have that, you need to create it like:

mix new minimal_server --sup

Ensure the following entry in mix.exs:

def application do
[
extra_applications: [:logger],
mod: {MinimalServer.Application, []}
]
end

and create the lib/minimal_server/application.ex file:

Libraries

In our mix.exs you will have to pull the following libraries:

defp deps do 
[
{:poison, "~> 3.0"},
{:plug, "~> 1.6"},
{:cowboy, "~> 2.4"}
]
end

And then, compile them:

mix do deps.get, deps.compile, compile

Endpoint

We are ready to build an entry point to your server. Let’s create lib/minimal_server/endoint.ex file then:

Plug provides Plug.Router to dispatch incoming requests based on the path and method. When the router is called, it will invoke the :match plug, represented by a match/2function responsible for finding a matching route, and then forward it to the :dispatch plug which will execute the matched code.

As we want our API to be JSON-compliant, we are implementing Plug.Parsers here. We will use it for parsing the request body because it handles application/json requests with the given :json_decoder.

Finally, we created a temporary “catch-all route” which matches all requests and respond with HTTP not found (404) status code.

Router

The router is the final step of our application. It’s the last part of the entire pipeline we created: from the web browser request to the response rendering.

We will handle the incoming call from a client and encode some message there:

In our Router above, a request will only match if it is a GET method and the route is /. The Router will reply with "application/json" content type and the body:

{
"response_type": "in_channel",
"text": "Hello from BOT :)"
}

Binding them together

Now is the time to extend our Endpoint to forward requests to the Router and extend our Application to spawn the Endpoint itself.

The first thing we can do by adding

forward(“/bot”, to: MinimalServer.Router)

line to the MinimalServer.Endpoint module. This will ensure all requests to /bot will be forwarded to and handled by our Router.

The second thing can be done by extending endpoint.ex with child_spec/1 and start_link/1 functions:

Now, you can modify your application.ex by adding MinimalServer.Endpoint to the list returned bychildren/0.

To start the server, it’s enough to execute:

mix run --no-halt

Finally, you can visit http://localhost:4000/bot and see our message :)

Deployment

Config

More often than not, you configure your server differently on the local environment and on production. That’s why we need to introduce separate settings for each of them. Firstly, let’s extend our general config.exs file like:

config :minimal_server, MinimalServer.Endpoint, port: 4000

In this case, test, prod and dev gets the same 4000 port unless overridden.

On production, we usually don’t want to hardcode the port value but rely on some system environmental variable like:

config :minimal_server, MinimalServer.Endpoint,
port: "PORT" |> System.get_env() |> String.to_integer()

After implementing that, we are sure that locally we use a fixed value and on production, we leverage environment-specific config.

Let’s inject this config in our endpoint.ex:

Heroku

Heroku offers the easiest one-click deployment without any complex adjustment. We just have to prepare a couple of simple files and create a remote application to deploy our project.

Once you have Heroku CLI installed, you can create a new app as follows:

heroku create minimal-server
Creating ⬢ minimal-server... done
https://minimal-server.herokuapp.com/ | https://git.heroku.com/minimal-server.git

Then, add the Elixir buildpack to your application:

heroku buildpacks:set \
https://github.com/HashNuke/heroku-buildpack-elixir.git

At the time of writing this article, the current versions of Elixir and Erlang are:

erlang_version=21.0
elixir_version=1.7.2

Put them in elixir_buildpack.config file to configure the buildpack itself.

The final step is to create aProcfile and, again, it is very simple:

web: mix run --no-halt

Once your files are added and committed, you can push them to Heroku:

git push heroku master
Initializing repository, done.
updating 'refs/heads/master'
...

And that’s it! Your app is available at https://minimal-server.herokuapp.com.

Subscribe to get the latest content immediately
https://tinyletter.com/KamilLelonek

Summary

By that time, you have the knowledge to implement a minimal JSON RESTful API and HTTP server in Elixir without any framework using just 3 libraries.

You don’t always need the entire Phoenix, regardless of how cool it is, neither any other framework, when you want to expose simple endpoints and provide basic routing.

I’m wondering why there isn’t any robust, well-tested and maintained framework between plug + cowboy and phoenix. Maybe there’s no real need to implement simple things? Maybe each company uses its own library? Or maybe everyone uses either Phoenix or the presented approach?

The entire repository is, as always, available on my GitHub:

--

--