Create an Elixir web app using Cowboy


This post is a part of my upcoming book on Phoenix 1.3.

By the end of this post you will learn

What is Cowboy?

Cowboy is a web server built in Erlang. It’s a production-ready server and not just a handy tool for development environment like webrick. Currently, Cowboy is the only supported Erlang web server by Phoenix. In this post, we shall see how to run a simple app using just Cowboy in your Elixir app. Understanding how Cowboy works and knowing the functionalities that it gives can form a strong foundation of understanding how Phoenix works and its internals.

Minimal code to start a Cowboy server and say Hello World!

Let’s start with creating a new Elixir app that will print “Hello World!” when you visit http://localhost:8080.

mix new my_elixir_web

Open up mix.exs and add Cowboy as a dependency.

# mix.exs
defp deps do
  [
    {:cowboy, "~> 1.0.0"},
  ]
end

Now run mix do deps.get, compile

Once the downloading of dependencies and compilation is complete, run iex shell using mix.

iex -S mix

This starts iex shell with all the compiled code of our application and its dependencies loaded. We don’t have any code in our application yet but we do have mentioned :cowboy as a dependency and we want it to be compiled and loaded in our iex shell.

Once in iex shell, define the following module and run the code below. I admit that the code is quite cryptic but don’t worry, I am going to explain it shortly. So go ahead and copy paste the following code in your iex shell.

defmodule CowboyHandler do  
  def init(_type, req, _opts) do
    {:ok, req, :nostate}
  end

  def handle(request, state) do    
    { :ok, reply } = :cowboy_req.reply(
      200, [{"content-type", "text/html"}], "<h1>Hello World!</h1>", request
    )
    {:ok, reply, state}
  end

  def terminate(_reason, _request, _state), do: :ok
end

dispatch_config = :cowboy_router.compile([
  { :_,
    [
      {:_, CowboyHandler, []},
    ]
  }
])

:cowboy.start_http(:http, 100,[{:port, 8080}],[{ :env, [{:dispatch, dispatch_config}]}])

Now open up http://localhost:8080 and behold the glamorous text of every programmer.

Cowboy Internals

To understand what we just did, we need to know more about Cowboy server. Cowboy is an Erlang web server similar to Nginx or Apache. However there are quite a few important differences.

A successful configuration of a Cowboy server to provide a response involves

  1. Defining the Cowboy router
  2. Compiling the router
  3. Defining the Cowboy Handler module
  4. Sending response in our Cowboy Handler
  5. Starting the Cowboy server with our compiled routes

Defining the Cowboy router

This is the most cryptic part of all, but understanding it is not as difficult as it seems.

# Code that maps any path to CowboyHandler module.

dispatch_config = :cowboy_router.compile([
  { :_,
    [
      {:_, CowboyHandler, []},
    ]
  }
])

Let’s crack it from the center. {:_, CowboyHandler, []} is Cowboy’s way of saying for any given path, use the module CowboyHandler as the handler and pass the value [].

{:_,  # --> matches any path. Cowboy uses :_ as wildcard
 CowboyHandler,  # --> Module that handles Cowboy request
 [],  # --> Arguments that go to the handler.
}

So each path in Cowboy is a tuple with three elements of the format {:path, handler, args}. Since an application might need to handle multiple paths, Cowboy wants the paths information as a list.

# eg., for multiple paths
[
  {"/", PageHandler, args},
  {"/about", AboutPageHandler, args},
  {"/contact", ContactPageHandler, args},
  {:_, Error404Handler, args}
]

Our simple Cowboy application, we need only one wildcard path as we want to display a static message to all paths.

[
  {:_, CowboyHandler, []},
]

Then to understand further, let’s go a level above the paths in our dispatch_config.

{ :_,
  [
    {:_, CowboyHandler, []},
  ]
}

# let me rewrite it as

{ :_,
  path_list # where path_list is a list of of paths as seen above.
}

This outer layer of tuple contains the host information. Again :_ is a wildcard but matching any host. So the host layer tuple is of the format {host, path_list}. Since there can be multiple hosts per machine, we provide a list here again. Following example makes the entire routing structure clear.

Imagine we have two different domains sub1.example.com and sub2.example2.com pointed to the same machine. On sub1.example1.com we need / and /about pages, and on sub2.example.com we need / and /contact pages. A configuration for such a system will look this:

[{
  "sub1.example.com",
  [{ "/",
      Sub1.HomePageHandler,
      []
    },
    { "/about",
      Sub1.AboutPageHandler,
      []      
    }
  ]
},
{
  "sub2.example.com",
  [{ "/",
      Sub2.HomePageHandler,
      []
    },
    { "/contact",
      Sub2.ContactPageHandler,
      []      
    }
  ]
},
]

Compiling the router

To compile our router, all that you need to do is call :cowboy_router.compile/1 with our list of routes. Compiling of routes enables Cowboy to match routes more efficiently.

routes_definition = [{host, path_list}]
compiled_router = :cowboy_router.compile(routes_definition)

Defining the Cowboy Handler module

The handler modules need to define the following three functions to be eligible to handle a Cowboy connection. In our CowboyHandler module above, we have defined the following three functions. Without any one of these three functions defined in your Handler module, Cowboy will not start.

def init({transport, protocol}, request, opts) do
  {:ok, request, state}
end

def handle(request, state) do
  {:ok, response, state}
end

def terminate(reason, request, state) do
 :ok
end

Sending response in our Cowboy Handler

The main function where the response goes out is the handle/2 and it needs to send out the response to Cowboy process by calling :cowboy_req.reply/4.

# template
:cowboy_req.reply(status_code, headers, body, request)

# example
:cowboy_req.reply(200, [{"content-type", "text/html"}], "<h1>Hello World!</h1>", request
)

Starting the Cowboy server

Nginx or Apache web servers are started by the host operating system. So when you deploy a PHP or Rails app, your application code has nothing to do with Nginx or Apache. Your application does not start or stop Nginx server and you don’t run an instance of Nginx for each of your application. If you have 10 PHP applications, you don’t run 10 separate copies of Nginx for these 10 web applications.

With Cowboy, your web application needs to start it as part of your application booting process and has to kill it when your application stops.

The below code that you have run already is responsible for starting Cowboy server.

:cowboy.start_http(:http, 100,[{:port, 8080}],[{ :env, [{:dispatch, dispatch_config}]}])

The code is starting 100 processes of Cowboy to handle multiple requests and is listening at port 8080.

To start the Cowboy server with our compiled routes, we need more details apart from the compiled router.

Are you using http or https? How many Cowboy processes do you need to start? Which port does Cowboy listen for requests?

:cowboy.start_http/4 function starts the Cowboy server with all required configuration.

:cowboy.start_http(ref_atom, pool_size, tcp_opts, cowboy_args)

With this above knowledge, we can now configure our Cowboy server:

:cowboy.start_http(:http, 100,[{:port, 8080}],[{ :env, [{:dispatch, dispatch_config}]}])

To recap, our entire code looks like this to display the simple “Hello World!” message. This entire code has to be run on iex -S mix shell.

# Our handler module
defmodule CowboyHandler do  
  def init(_type, req, _opts) do
    {:ok, req, :nostate}
  end

  def handle(request, state) do
    # Sending reply to the browser
    { :ok, reply } = :cowboy_req.reply(
      200, [{"content-type", "text/html"}], "<h1>Hello World!</h1>", request
    )
    {:ok, reply, state}
  end

  def terminate(_reason, _request, _state), do: :ok
end

# Configuring and compiling router
dispatch_config = :cowboy_router.compile([
  { :_,
    [
      {:_, CowboyHandler, []},
    ]
  }
])

# Starting the Cowboy server with our dispatch_config
:cowboy.start_http(:http, 100,[{:port, 8080}],[{ :env, [{:dispatch, dispatch_config}]}])

We could improve on this by having the code saved in a module file and starting the Cowboy server as part of our app starting process. I leave it as an exercise for you to handle it.

Why do you need Phoenix?

Can you build your app entirely on Cowboy using the method described above? If so, what do you need?

  1. You might need more specific routes than the one we configured. Yes, this can be done. The router will look more complex and difficult to manage but possible.
  2. You will need support for more HTTP verbs. All routes that we saw above are HTTP GET requests. We need support for POST, PUT, etc., for a real-world application. Again, Cowboy provides this support, but it just needs modification to the way we started our Cowboy server.
  3. You need models and ‘ActiveRecord-like’ features. For this, you can use the Ecto package (ActiveRecord equivalent in Elixir).
  4. You need templates, helper functions to do many of the common web tasks like getting and setting cookies, parsing headers, CSRF checks, etc. Cowboy doesn’t do all of these.
  5. Lastly, you need to have a good understanding of Erlang so that you can read Cowboy documentation, which by all means is very sparse compared to the rich docs in the Elixir ecosystem.

Ok. Solutions exist, but do you want to take that route? Unless you are venturing out to build another Phoenix framework or you are a masochist, using the Phoenix library is the right choice to focus on getting things done and to keep your sanity level in check.

Here is a non-exhaustive list of what Phoenix provides in comparison with Cowboy:

That brings us to the end of a long post. Hope you enjoyed it. If you have any questions, please feel free to comment below, and I will do my best to answer them all.