Create an Elixir web app using Cowboy

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

Subscribe to get notified when my book is ready:

By the end of this post you will learn

  • What is Cowboy?
  • Minimal code to say "Hello World!" on the browser and in that process understand the Cowboy internals.
  • Understand what flexibility Plug/Phoenix provides and why you need it instead of using just Cowboy.

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.

Phoenix Stack

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.

  • The web server maps http requests to an Erlang module.
  • The web server is started by the application and not by the operating system.

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, you application code has nothing to do with Nginx or Apache. You 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, you web application need 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}], [...omitted])

The code is starting 100 processes of Cowboy to handle multiple request 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 request?

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

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

  • ref_atom - you can provide any atom. Ranch, a dependency of Cowboy makes use of this atom to name the process that it manages.
  • pool_size - how many Cowboy process do you need for your application to handle multiple concurrent requests?
  • tcp_opts - there are many tcp options that you can configure when you start the server. One such is configuring the port number on which your server listens for request. Since there are many options, the data type is a list of tuple with each tuple configuring one tcp option.
  • cowboy_args - these are data that are passed to Cowboy for managing a request. It contains the routing information.

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 one that 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 case 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 Ecto package (AR equivalent in Elixir).
  4. You need templates, helper functions to do many of the common web stuff like getting and setting cookies, parsing headers, csrf checks etc. Cowboy doesn't do all of these.
  5. Lastly you need to have good understanding of Erlang so that you can read Cowboy documentation which by all means is very sparse compared to the rich docs in 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 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?

  • Phoenix uses a library called Plug, that takes care of low-level plumbing with Cowboy and frees you from worrying about how to configure Cowboy.
  • Your router configurations are much cleaner in Phoenix. Thanks for meta programming in Elixir, you can write routes without making any one mad.
  • Plug provides several small independent functionalities that are common in web so you can pick and choose what you need with no bloat.
  • Phoenix which is build on top of Plug, provides a clean and uniform way to deal with request and if you could understand how Plug works, you can understand most of Phoenix.
  • With Phoenix, you can get most of your tasks done without touching a line of Erlang code and do everything with Elixir. That's a great boost to productivity.

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.

I am writing a book on Phoenix 1.3.
Subscribe to get notified.



Do you find this post interesting? Have something to share? Please comment below and start a conversation with me