Simple web services with Ace + Elixir

Ace is a featherweight toolkit for developing web applications in Elixir. Many languages have similar projects, such as Sinatra for Ruby and Flask for Python. Such focused toolkits are great for moving quickly on simple applications.

Elixir already has a fully fledged web framework, Phoenix. Phoenix comes with JavaScript bundling, HTML templating, websocket channels and database drivers. If this is more than your project needs you might like working with Ace.

This post shows the process of creating a simple greetings application with Ace. Or, jump straight to the code.

To follow along Elixir 1.5+ is needed.

Creating a project

First set up the project using mix.

$ mix new greetings --sup $ cd greetings

Ace is the only dependency needed in this project, add it in the generated mix.exs file.

# ./mix.exs defmodule Greetings.Mixfile do use Mix.Project # ... other project configuration defp deps do [ {:ace, "~> 0.15.10"}, ] end end

The latest version of Ace is available at hex.pm.

Use the mix command to fetch project .

$ mix deps.get

Hello, World!

The first thing a greetings service should do is say "Hello, World!" .

# ./lib/greetings.ex defmodule Greetings do use Ace.HTTP.Service, cleartext: true # 1 def handle_request(_request, _state) do response(:ok) # 2 |> set_header("content-type", "text/plain") |> set_body("Hello, World!") end end

Define a service using Ace.HTTP.Service , the list of default options says this service will start using http. For any request this service will return a 200 OK response.

Starting a service

An instance of the greeting service is started using Greetings.start_link/2 . The greeting application is configured with the first argument to start_link . Ace server options, such as port, are passed as a list of options as the second argument.

The service can be started manually from an iex session.

$ iex -S mix iex> Greetings.start_link(nil, port: 8080) [info] Serving cleartext using HTTP/1 on port 8080 {:ok, #PID }

At this point we can get a greeting. In a second terminal session:

$ curl http://localhost:8080 Hello, World!

Or view in the browser; http://localhost:8080

Dynamic endpoints

To personalise the service an endpoint for greeting a given name is needed. By matching properties from the request struct, we can separate handling for this new endpoint.

# ./lib/greetings.ex defmodule Greetings do use Ace.HTTP.Service, [cleartext: true] def handle_request( %{method: :GET, path: []}, # 1 _state) do response(:ok) |> set_header("content-type", "text/plain") |> set_body("Hello, World!") end def handle_request( %{method: :GET, path: ["name", name]}, # 2 _state) do response(:ok) |> set_header("content-type", "text/plain") |> set_body("Hello, #{name}!") end def handle_request(_request, _state) do # 3 response(:not_found) |> set_header("content-type", "text/plain") |> set_body("Sorry, nothing here.") end end

handle any GET requests to the service root. The second match has a variable path segment, it handles all GET requests to /name/:name Return a 404 response for any request not matched so far.

NOTE: A request's path is split into a list of segments to ease matching. Therefore an empty list is for / ; and /foo/bar becomes ["foo", "bar"]

Configuring the service

Making the greeting configurable will increase the service's flexibility. The configuration provided when starting the service is accessible as the second argument to the handle_request/2 callback.

# ./lib/greetings.ex defmodule Greetings do use Ace.HTTP.Service, [cleartext: true] def handle_request( %{method: :GET, path: []}, %{greeting: greeting}) # 1 do response(:ok) |> set_header("content-type", "text/plain") |> set_body("#{greeting}, World!") # 2 end # ... other endpoints end

Pattern matching to extract required configuration for endpoint. Generate message from configured value.

Now when starting the service the correct config must be given.

iex> Greetings.start_link(%{greeting: "Oi"}, port: 8080) [info] Serving cleartext using HTTP/1 on port 8080 {:ok, #PID }

Supervising a service

A production ready greetings service should be started and supervised by the OTP application. The Greetings module and list of starting arguments are added to the project's children. The service will now be started when the OTP application is run.

# ./lib/greetings/application.ex defmodule Greetings.Application do @moduledoc false use Application def start(_type, _args) do greeting = System.get_env("GREETING") || "Hello" children = [ {Greetings, [%{greeting: greeting}, [port: 8080]]}, ] opts = [strategy: :one_for_one, name: Greetings.Supervisor] Supervisor.start_link(children, opts) end end

At this point we no longer need to start the service manually from an iex session. It will be started automatically when running the project with mix.

$ GREETING=Haigh mix run --no-halt

Fin

There you have it, the simplicity of building web services with Ace.