Hello World web app in Elixir, part 1 - Cowboy

A software developer, like any other social animal, wants to feel accepted by its herd. Among many ways of achieving that, keeping up with the trends in an ever-changing industry is essential. In an attempt at being a trendy Ruby dev, I decided to learn Elixir, and because I am a web developer, I want to know how to build web apps.

But before I dive into the high-level web framework Phoenix, I want to get to know the lower-level parts first. Hence I will build three identical apps in three different variants - each using a set of tools that extends the previous one.

Specifications

Not to overcomplicate matters, my Hello World app will respond with plain text and will have those endpoints:

  • GET /hello returns a 200 plain text response "Hello, World!"
  • GET /hello/:name returns a 200 plain text response "Hello, #{name}!"
  • Anything else returns a 404 plain text response "Goodbye!"

Cowboy

Cowboy is an Erlang HTTP server. Which won’t stop us from using it because “Elixir provides excellent interoperability with Erlang libraries”. In fact, it is the web server that Plug and, by extension, Phoenix use.

Mixfile

I am going to use the newest version of Cowboy. Currently, it’s 2.0.0-pre.3. It hasn’t been published to Hex yet, so I need to fetch it from GitHub. I am also adding Remix which is a little tool that will recompile my code on any changes to the source files.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# mix.exs
defmodule HelloWorld.Mixfile do
  use Mix.Project

  def project do
    [
      app: :hello_world,
      version: "0.1.0",
      elixir: "~> 1.3",
      deps: deps
    ]
  end

  def application do
    [
      mod: {HelloWorld, []},
      applications: applications(Mix.env)
    ]
  end

  defp applications(:dev), do: applications(:all) ++ [:remix]
  defp applications(_), do: [:cowboy]

  defp deps do
    [
      {:cowboy, tag: "2.0.0-pre.3",
        git: "https://github.com/ninenines/cowboy"},
      {:remix, "~> 0.0.1", only: :dev}
    ]
  end
end

This line means that a HelloWorld module is going to be the entry point to my project, and it needs to define a start/2 function:

1
mod: {HelloWorld, []}

I only want to use Remix in the development environment, so I need to define my application’s applications in an environment-aware way:

1
2
3
4
5
6
def application do
  [applications: applications(Mix.env)]
end
  
defp applications(:dev), do: applications(:all) ++ [:remix]
defp applications(_), do: [:cowboy]

Config

I do not want to hardcode the port number in the source code, so I am adding it to the config file.

1
2
3
4
# config/config.exs
use Mix.Config

config :hello_world, port: 8001

HelloWorld

I am going to follow the Cowboy’s guide on how to listen for connections. That’s not exactly trivial, because the guide is in Erlang, and I do not know Erlang. Luckily I had some assistance from a helpful and more skilled co-worker of mine.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
defmodule HelloWorld do
  require Logger
  use Application

  def start(_type, _args) do
    port = Application.get_env(:hello_world, :port)

    path_list = [
      {"/hello/[:name]", HelloWorld.HelloHandler, []},
      {"/[...]", HelloWorld.GoodbyeHandler, []},
    ]

    routes = [{:_, path_list}]
    dispatch = :cowboy_router.compile(routes)
    opts = [port: port]
    env = [dispatch: dispatch]
    onresponse = fn(status, _headers, _body, request) ->
      method = :cowboy_req.method(request)
      path = :cowboy_req.path(request)
      Logger.info("#{method} #{path} - #{status}")
      request
    end

    :cowboy.start_http(:http, 100, opts,
      [env: env, onresponse: onresponse])
  end
end

This is how to read values form the config files:

1
port = Application.get_env(:hello_world, :port)

Cowboy needs a mapping of host/path matches to modules that handle the request. This will match the paths in path_list for all host names:

1
routes = [{:_, path_list}]

The path list is a list of 3-tuples:

1
2
3
4
path_list = [
 {"/hello/[:name]", HelloWorld.HelloHandler, []},
 {"/[...]", HelloWorld.GoodbyeHandler, []},
]

The first element in the tuple is a path matcher. :name will capture a segment and store it under the key :name. A segment is a part of the path between two slashes. Surrounding :name with [] makes it optional. [...] will match everything to the end of the path.

The second element is the name of the module that will handle the request. The handler has to implement a init/2 function which responds to requests. This function has to return a 3-tuple, where the first element is the atom :ok, the second is the request, and the third is a state that would be passed to subsequent callbacks, except that usually plain HTTP handlers do not have callbacks other than init.

The third element is a list of options that will be passed to the handler’s init function as the second argument.

Notice that there is no matching on request methods anywhere. I will have to check the request’s method in HelloHandler myself because I only want to allow GET requests.

To log responses, I’m defining a onresponse hook. It has to return the request.

1
2
3
4
5
6
onresponse = fn(status, _headers, _body, request) ->
  method = :cowboy_req.method(request)
  path = :cowboy_req.path(request)
  Logger.info("#{method} #{path} - #{status}")
  request
end

Then I’m finally starting the HTTP server:

1
2
:cowboy.start_http(:http, 100, opts,
  [env: env, onresponse: onresponse])

The second argument here is the number of acceptor processes - processes that will wait for connections only to spawn a new process that will handle the connection.

HelloHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
defmodule HelloWorld.HelloHandler do
  def init(request, options) do
    if (:cowboy_req.method(request) == "GET") do
      name = :cowboy_req.binding(:name, request, "World")
      headers = [{"content-type", "text/plain"}]
      body = "Hello, #{String.capitalize(name)}!"

      request2 = :cowboy_req.reply(200, headers, body, request)
      {:ok, request2, options}
    else
      HelloWorld.GoodbyeHandler.init(request, options)
    end
  end
end

I’m “redirecting” the request to the other handler if it’s not a GET request:

1
2
3
4
5
    if (:cowboy_req.method(request) == "GET") do
     # code ommited
    else
      HelloWorld.GoodbyeHandler.init(request, options)
    end

This line reads the value under the key :name captured from the path. If it does not exist, it returns the default value “World”:

1
name = :cowboy_req.binding(:name, request, "World")

:cowboy_req.reply returns a modified request object that I have to return in my handler’s init function.

GoodbyeHandler

1
2
3
4
5
6
7
8
9
defmodule HelloWorld.GoodbyeHandler do
  def init(request, options) do
    headers = [{"content-type", "text/plain"}]
    body = "Goodbye!"

    request2 = :cowboy_req.reply(404, headers, body, request)
    {:ok, request2, options}
  end
end

Running the app

I am starting the app with mix run --no-halt and trying it out:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ curl -w "\n%{http_code}\n" http://localhost:8001/hello
Hello, World!
200

$ curl -w "\n%{http_code}\n" http://localhost:8001/hello/reader
Hello, Reader!
200

$ curl -X PUT -w "\n%{http_code}\n" http://localhost:8001/hello
Goodbye!
404

$ curl -w "\n%{http_code}\n" http://localhost:8001/banana
Goodbye!
404

Works as expected!

Observer

For debugging purposes, it’s useful to know about a tool called Observer. If I start my app with iex -S mix instead, I can observe it.

1
iex(1)> :observer.start

This command should open a window where I can see, among many other things, my app’s process tree:

The app's process tree
Not much here, huh?

But wait, that’s all? Where are those acceptor processes I have presumably run? Double-clicking on the process <0.142.0> reveals its details. It has a link to <0.143.0>.

Process information
I don't know who you are, <0.143.0>, but I will find you and I will inspect your tree.

It turns out <0.143.0> is a part of an application used internally by Cowboy - Ranch. I can see that there are in fact 100 acceptor processes there, waiting for connections.

Ranch's process tree
Here they are! All 100 of them (notice the long scrollbar).

It’s not exactly important at the moment, it’s just a little trick that might come in handy once I start building more complex apps.