Hello World web app in Elixir, part 2 - Plug
This is a continuation of Hello World web app in Elixir, part 1 - Cowboy. I am going to reference that post a few times because there are many similarities between the two apps, so you should probably read that first.
Plug
Plug is a little bit like Ruby’s Rack. It provides adapters for different web servers, similar to Rack handlers. It also provides a specification for writing composable modules, similar to Rack middleware. A nice introduction to Plug can be found here.
Mixfile
It’s very similar to the first one. I added Plug to both deps
and applications
. I am also using a stable older version of Cowboy, as advised in Plug’s docs.
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
# 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, :plug]
defp deps do
[
{:cowboy, "~> 1.0.0"},
{:plug, "~> 1.0"},
{:remix, "~> 0.0.1", only: :dev}
]
end
end
Config
Again, almost identical as the first one except for a different port number (8001 -> 8002
), to be able to run both apps at the same time.
1
2
3
# config/config.exs
use Mix.Config
config :hello_world, port: 8002
HelloWorld
1
2
3
4
5
6
7
8
9
10
# lib/hello_world.ex
defmodule HelloWorld do
use Application
def start(_type, _args) do
port = Application.get_env(:hello_world, :port)
Plug.Adapters.Cowboy.http(HelloWorld.Router, [],
[port: port])
end
end
I am starting the Cowboy’s HTTP server via the Plug’s adapter.
1
Plug.Adapters.Cowboy.http(HelloWorld.Router, [], [port: port])
The first argument, HelloWorld.Router
, is a module that adheres to the Plug’s specification. The second argument, []
, is a list of options that will be passed to the module’s init
function. The third argument is a list of options for the Cowboy’s HTTP server.
I will not write a plug module from scratch myself. There already are a lot of plugs ready to be used. I will use the Plug.Router to match paths to responses.
HelloWorld.Router
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
32
33
34
35
36
# lib/hello_world/router.ex
defmodule HelloWorld.Router do
use Plug.Router
plug Plug.Logger
plug :match
plug :dispatch
get "/hello" do
hello(conn)
end
get "/hello/:name" do
hello(conn, name)
end
match _ do
goodbye(conn)
end
defp hello(conn, name \\ "World") do
body = "Hello, #{String.capitalize(name)}!"
conn
|> put_resp_content_type("text/plain")
|> send_resp(200, body)
end
defp goodbye(conn) do
body = "Goodbye!"
conn
|> put_resp_content_type("text/plain")
|> send_resp(404, body)
end
end
The router is a plug itself, but also has its own plug pipeline. This one line takes care of logging:
1
plug Plug.Logger
Those two plugs are necessary to be able to use the Plug’s router DSL:
1
2
plug :match
plug :dispatch
I’m using the get
macro to match GET
requests:
1
2
3
4
5
6
7
get "/hello" do
hello(conn)
end
get "/hello/:name" do
hello(conn, name)
end
As far as I know there is no way to define an optional segment in the path, so I need to match the two paths separately and use a hello
function with an argument with a default value to respond to the requests with plain text:
1
2
3
4
5
6
7
defp hello(conn, name \\ "World") do
body = "Hello, #{String.capitalize(name)}!"
conn
|> put_resp_content_type("text/plain")
|> send_resp(200, body)
ene
Here I’m using the match
macro to match all the other paths and methods:
1
2
3
match _ do
goodbye(conn)
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:8002/hello
Hello, World!
200
$ curl -w "\n%{http_code}\n" http://localhost:8002/hello/john
Hello, John!
200
$ curl -X POST -w "\n%{http_code}\n" http://localhost:8002/hello
Goodbye!
404
$ curl -w "\n%{http_code}\n" http://localhost:8002/hi
Goodbye!
404