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 conn
s. 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.