Table of Contents
Web Development with Elixir
What you need
- Some Elixir knowledge. If you don’t, take a look at the Getting Started section of http://elixir-lang.org to get going.
- Elixir v1.0.0 installed. If you don’t, see Installing Elixir.
Building a Basic Application
Starting with any project in a new-to-you language, a big question is usually, “Where do I put my files?” Well, fret not! We’ll use Mix to generate a project skeleton for us.
Mix is a build tool that provides tasks for creating, compiling, testing Elixir projects, as well as handle dependencies, and more.
Let’s create our project with mix new:
1 $ mix new hello_world --sup
2 * creating README.md
3 * creating .gitignore
4 * creating mix.exs
5 * creating config
6 * creating config/config.exs
7 * creating lib
8 * creating lib/hello_world.ex
9 * creating test
10 * creating test/test_helper.exs
11 * creating test/hello_world_test.exs
12
13 Your mix project was created successfully.
14 You can use mix to compile it, test it, and more:
15
16 cd hello_world
17 mix test
18
19 Run `mix help` for more commands.
20
21 $ cd hello_world
We passed two arguments to the mix new task: a project name in snake case (hello_world) and the --sup flag. Mix used the project name as our OTP application name and converted it to camel case (HelloWorld) for use in module names when creating our project. The --sup flag let Mix know that we wanted it to create an OTP supervisor and an OTP application callback in our main HelloWorld module.
Follow along in your own environment, or pull down the project from the HelloWorld project page.
Mixing It Up
Moving on into the future, Mix will help us with our dependencies, testing our app, running our app, and more, but how does Mix know enough about our project to help us so much? Our Mixfile, of course! Let’s open our mix.exs to take a look:
1 defmodule HelloWorld.Mixfile do
2 use Mix.Project
3
4 def project do
5 [app: :hello_world,
6 version: "0.0.1",
7 elixir: "~> 1.0.0",
8 deps: deps]
9 end
10
11 def application do
12 [applications: [:logger],
13 mod: {HelloWorld, []}]
14 end
15
16 defp deps do
17 []
18 end
19 end
Two key things to look at are project/0 and application/0 (the project and application functions).
project/0 provides your project’s configuration (in keyword list form) to Mix.app: :hello_world sets our application’s name in Erlang/OTP land.
version: "0.0.1" sets our application’s version, and elixir: "~> 1.0.0" sets the version of Elixir on which our application depends. All versions in the Elixir world typically follow semantic versioning, so keep that in mind when you’re dealing with them.
deps: deps sets our project’s dependencies with the deps/0 function, which is currently an empty list. Using the Hex Package manager, dependencies are added as two-item tuples like {:dependency, "~> 1.0.0"}.
application/0 provides your project’s OTP application configuration (in keyword list form) to Mix. applications: [:logger] lists the OTP applications on which your project depends. These applications will be started for you automatically when your application runs.
mod: {HelloWorld, []} provides details for OTP to run your application. With Mix building your project’s skeleton, you shouldn’t need to change this, but the first element in the tuple is the module that contains the start/2 callback function for the Application behaviour, which in our case is HelloWorld. If you ever rename your project and/or rename/refactor your modules, be sure to update this line to reflect any changes.
Getting Down to Business
Now for us to get a web application running, we’ll need a server that can speak HTTP, so it’s time for you to build a HTTP/1.1 compliant server. You have fun with that. I’ll wait.
Don’t want to do that just to write a “Hello World” app? I don’t blame you, so instead of writing our own, let’s use Cowboy, a very popular Erlang web server. First thing’s first. Let’s add Cowboy as a dependency in our mix.exs file:
1 defp deps do
2 [{:cowboy, "~> 1.0.0"}]
3 end
and to prepare a bit for the future, we need to add :cowboy under applications in application/0:
1 def application do
2 [applications: [:logger, :cowboy],
3 mod: {HelloWorld, []}]
4 end
Now, let’s pull down our deps from Hex:
1 $ mix deps.get
2 Running dependency resolution
3 Unlocked: cowboy
4 Dependency resolution completed successfully
5 cowlib: v1.0.0
6 cowboy: v1.0.0
7 ranch: v1.0.0
8 * Getting cowboy (package)
9 Checking package (https://s3.amazonaws.com/s3.hex.pm/tarballs/cowboy-1.0.0.tar)
10 Using locally cached package
11 Unpacked package tarball (/Users/shane/.hex/packages/cowboy-1.0.0.tar)
12 * Getting cowlib (package)
13 Checking package (https://s3.amazonaws.com/s3.hex.pm/tarballs/cowlib-1.0.0.tar)
14 Using locally cached package
15 Unpacked package tarball (/Users/shane/.hex/packages/cowlib-1.0.0.tar)
16 * Getting ranch (package)
17 Checking package (https://s3.amazonaws.com/s3.hex.pm/tarballs/ranch-1.0.0.tar)
18 Using locally cached package
19 Unpacked package tarball (/Users/shane/.hex/packages/ranch-1.0.0.tar)
Thanks to the power of Hex and S3, that shouldn’t have taken long. We’re now able to implement Cowboy into our project to leverage all of the goodness it provides.
Adding the Fun (but Necessary) Bits
Open up lib/hello_world.ex so we can get to work. Most of the work here will entail getting Cowboy set up so that it will listen for incoming requests. Even though we have set up our mix.exs file to start the :cowboy OTP application, Cowboy doesn’t start HTTP or HTTPS listeners by default and relys on developers (us) to do so.
We’ll create a helper function that defines a set of routes for our application, compile those routes into a form the Cowboy dispatcher knows how to use, and finally, start the HTTP listener with some basic options.
1 def run do
2 # Routes for our application
3 routes = [
4 {"/", HelloWorld.Handler, []}
5 ]
6
7 # Compile our routes so Cowboy knows
8 # how to dispatch requests
9 dispatch = :cowboy_router.compile([{:_, routes}])
10
11 # Set some options
12 opts = [port: 8080]
13 env = [dispatch: dispatch]
14 {:ok, _pid} = :cowboy.start_http(:http, 100, opts, [env: env])
15 end
Optionally, we can add a worker child for our supervisor tree to run this function automatically when our application starts, otherwise, the function will need to be run manually, such as in an IEx session.
Resulting in:
1 defmodule HelloWorld do
2 use Application
3
4 def start(_type, _args) do
5 import Supervisor.Spec, warn: false
6
7 children = [
8 # Optional worker child for HelloWorld.run/0
9 worker(__MODULE__, [], function: :run)
10 ]
11
12 opts = [strategy: :one_for_one, name: HelloWorld.Supervisor]
13 Supervisor.start_link(children, opts)
14 end
15
16 def run do
17 routes = [
18 {"/", HelloWorld.Handler, []}
19 ]
20 dispatch = :cowboy_router.compile([{:_, routes}])
21 opts = [port: 8080]
22 env = [dispatch: dispatch]
23 {:ok, _pid} = :cowboy.start_http(:http, 100, opts, [env: env])
24 end
25 end
When called, run/0 will allow our application to respond to all requests to http://localhost:8080/.
Handle Yourself Properly
We defined a singular route that has some sort of relation to an undefined HelloWorld.Handler module, but what should be in this module? Create lib/hello_world/handler.ex, and put this in it:
1 defmodule HelloWorld.Handler do
2 def init({:tcp, :http}, req, opts) do
3 headers = [{"content-type", "text/plain"}]
4 body = "Hello world!"
5 {:ok, resp} = :cowboy_req.reply(200, headers, body, req)
6 {:ok, resp, opts}
7 end
8
9 def handle(req, state) do
10 {:ok, req, state}
11 end
12
13 def terminate(_reason, _req, _state) do
14 :ok
15 end
16 end
init/3 handles the bulk of the work here. It let’s Cowboy know what types of connections we wish to handle with this module (HTTP via TCP) and actually creates a response for each incoming request. We use :cowboy_req.reply/4 to build our response with a status code, a list of headers, a response body, and the request itself as Cowboy stashes supporting information about the request in that variable.
handle/2 and terminate/3 aren’t terribly useful in this example, but in other cases, they offer the means to control the lifespan of the Erlang process that is spawned to handle the request. For now, consider them necessary boilerplate code.
Running Our Marvelous Work
Now’s the time for our hard work to pay off. Pass mix as a script to IEx with iex -S mix:
1 $ iex -S mix
2 Erlang/OTP 17 [erts-6.2] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [\
3 kernel-poll:false] [dtrace]
4
5 ==> ranch (compile)
6 Compiled src/ranch_transport.erl
7 Compiled src/ranch_sup.erl
8 Compiled src/ranch_ssl.erl
9 Compiled src/ranch_tcp.erl
10 Compiled src/ranch_protocol.erl
11 Compiled src/ranch_listener_sup.erl
12 Compiled src/ranch_app.erl
13 Compiled src/ranch_acceptors_sup.erl
14 Compiled src/ranch_acceptor.erl
15 Compiled src/ranch.erl
16 Compiled src/ranch_server.erl
17 Compiled src/ranch_conns_sup.erl
18 ==> cowlib (compile)
19 Compiled src/cow_qs.erl
20 Compiled src/cow_spdy.erl
21 Compiled src/cow_multipart.erl
22 Compiled src/cow_http_te.erl
23 Compiled src/cow_http_hd.erl
24 Compiled src/cow_date.erl
25 Compiled src/cow_http.erl
26 Compiled src/cow_cookie.erl
27 Compiled src/cow_mimetypes.erl
28 ==> cowboy (compile)
29 Compiled src/cowboy_sub_protocol.erl
30 Compiled src/cowboy_middleware.erl
31 Compiled src/cowboy_websocket_handler.erl
32 Compiled src/cowboy_sup.erl
33 Compiled src/cowboy_static.erl
34 Compiled src/cowboy_spdy.erl
35 Compiled src/cowboy_router.erl
36 Compiled src/cowboy_websocket.erl
37 Compiled src/cowboy_rest.erl
38 Compiled src/cowboy_loop_handler.erl
39 Compiled src/cowboy_protocol.erl
40 Compiled src/cowboy_http_handler.erl
41 Compiled src/cowboy_handler.erl
42 Compiled src/cowboy_clock.erl
43 Compiled src/cowboy_bstr.erl
44 Compiled src/cowboy_app.erl
45 Compiled src/cowboy.erl
46 Compiled src/cowboy_http.erl
47 Compiled src/cowboy_req.erl
48 Compiled lib/hello_world/handler.ex
49 Compiled lib/hello_world.ex
50 Generated hello_world.app
51 Interactive Elixir (1.0.0) - press Ctrl+C to exit (type h() ENTER for help)
52 iex(1)>
If you didn’t add HelloWorld.run/0 as a worker child in the supervisor tree, be sure to run that in the IEx console.
1 iex(1)> HelloWorld.run
2 {:ok, #PID<0.141.0>}
3 iex(2)>
From here, you should be able to open a browser and point it to http://localhost:8080/ to see the “Hello world!” message presented to us from our handler.
1 $ curl -i http://localhost:8080
2 HTTP/1.1 200 OK
3 connection: keep-alive
4 server: Cowboy
5 date: Tue, 14 Oct 2014 00:52:09 GMT
6 content-length: 12
7 content-type: text/plain
8
9 Hello world!
Check that out. Way to go! You now have the knowledge to create a basic web application, but I bet you’re saying something like, “There has to be something more.”
You’re right.
The Something More
While interoperating with Erlang code can provide many benefits, the resulting Elixir code can end up very non-idiomatic, and the necessary bits needed to work with Cowboy directly adds some bloat to our project which would otherwise be fairly lean.
So what’s the something more? Meet Plug.
Plug In
What’s Plug? Let’s see what the project’s code repository has to say:
Plug is:
- A specification for composable modules in between web applications
- Connection adapters for different web servers in the Erlang VM
We should deconstruct those statements.
A Specification
Part of what lets Plug is its specification, letting implementors know what is given to and expected from a plug a.k.a. a composable module. According to the spec, there are two types of plugs: function and module.
Function plugs are single functions that follow the signature
(Plug.Conn.t, Plug.opts) :: Plug.Conn.t. In other words, a function
plug accepts a connection and an optional set of options for the plug
to use, which can be any term that is a tuple, atom, integer, float,
or list of the aforementioned types, and returns a connection.
Module plugs operate similarly, requiring a public function call/2
that follows the same signature as a function plug. Module plugs also
require an info/1 function to be use to preprocess options sent to
it. Often, call/2 is invoked as call(conn, info(opts)), especially
(as we’ll see later on) when a plug stack is compiled.
Connection Adapters
With Erlang being great for scalable network programming, it seems
only natural that people wanted to use it for web programming over
the years since Erlang’s release in the mid-1980s. Erlang’s standard
installation comes with a long list of modules, including one for
creating HTTP servers, but the httpd module isn’t really
made for production-level servers.
Because of that, many people have developed some awesome HTTP servers over the years, including Cowboy, Mochiweb, Yaws, Elli, and Misultin. Wouldn’t it be great if we could change the web server used in our application based on needs or even on a whim? Well, you’re in luck because that is part of what Plug is trying to achieve with connection adapters.
Currently, Plug only (officially) supports Cowboy,
but there is an open PR for adding an adapter for
Elli, with others probably hidden around the internets. If you ever
wanted to use an Erlang or Elixir web server with Plug that isn’t
supported, all you would need to do is make sure you implemented
the Plug.Conn.Adapter behaviour.
Simple, huh?
Ok, enough with the yammering. Let’s get back into some code.
Route to Your Heart
One benefit with using Plug is Plug.Router, a routing DSL with
some visual similarities to Sinatra. It really takes the
pain away from compiling a dispatch list using the raw, Erlang term
based format that Cowboy expects.
1 defmodule HelloWorldPlug.Router do
2 use Plug.Router
3
4 plug :match
5 plug :dispatch
6
7 get "/" do
8 conn
9 end
10 end
See? Isn’t that better? Plug.Router has macros for handling GET,
POST, PUT, PATCH, DELETE, and OPTIONS requests, with a
general match macro that can be used to handle other HTTP methods
you may require for your application.
Connecting Connections
Looking back at the previous code snippet (the route), you may have
some questions. What’s that conn thing? Why aren’t we doing
anything with it? Why do we just return it? These would be
perfectly good things to ask considering we basically glanced over
the code quickly before.
Essentially, the code let’s us know that we expect our application
to handle GET requests to / (the root). The get macro we use
injects a conn variable into the local with some macro magic (we
will learn a little about this later), and because, for the moment,
we’re lazy and don’t know any better, we let the conn pass
through unmodified as our return expression.
Wait. What’s that? You don’t want to be lazy anymore and want to send a message to your applications visitors? Let’s say hi!
1 get "/" do
2 conn
3 |> Plug.Conn.send_resp(200, "Hello world!")
4 end
Here, we use send_resp/3 from the Plug.Conn module to send a
200 OK response with our message. Plug.Conn has a variety of
functions for reading from and creating new conns. We will touch
on a lot of these as we progress, but if you’re interested now,
take a look to see what it has to offer.