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
    
  1. Define a service using Ace.HTTP.Service, the list of default options says this service will start using http.
  2. 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
    
  1. handle any GET requests to the service root.
  2. The second match has a variable path segment, it handles all GET requests to /name/:name
  3. 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
    
  1. Pattern matching to extract required configuration for endpoint.
  2. 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.