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.
- Much more is possible with Ace, see the documentation.
- For components that work on top of Ace see the Raxx project and associated middlewares.
- See the complete greetings application on github