Today we'll change gears a bit: It's time to make a web app. We'll need a project to work on for later articles in order to explore how to work with Elixir code, and so for this article we'll dive into some of the different ways to build a web app in Elixir. Our goal will be a simple example of a web app, and ideally it should make a data database call to be representative of real-world projects. Let's see what solutions we can find.
Table of Contents
Nothing At All
What if we don't choose a solution? It's technically possible to serve HTML directly using the Erlang TCP/IP socket module, and although this isn't a viable solution it might be illuminating to see what it takes to work without abstractions.
The basic idea is to first listen on a TCP socket and wait for traffic, then send data when a request arrives. Here's a script that does that, full of comments to explain the details:
defmodule SimpleServer do
def start(port) do
# Listen on a TCP socket on the specified port
# :binary - Treat data as raw binary, instead of
# being automatically converted into
# Elixir strings (which are UTF-8 encoded).
# It'd be unnecessary to convert, as the
# HTTP protocol uses raw bytes.
# packet: :line - Frame messages using newline delimiters,
# which is the expected shape of HTTP-data
# active: false - Require manual fetching of messages. In
# Erlang, active mode controls the
# automatic sending of messages to the
# socket's controlling process. We disable
# this behavior, so our server can control
# when and how it reads data
{:ok, socket} = :gen_tcp.listen(port, [
:binary, packet: :line, active: false
])
IO.puts("Listening on port #{port}")
loop_handle_client_connection(socket)
end
defp loop_handle_client_connection(socket) do
# Wait for a new client connection. This is a blocking call
# that waits until a new connection arrives.
# A connection returns a `client_socket` which is connected
# to the client, so we can send a reply back.
{:ok, client_socket} = :gen_tcp.accept(socket)
send_hello_world_response(client_socket)
:gen_tcp.close(client_socket)
# Recursively wait for the next client connection
loop_handle_client_connection(socket)
end
defp send_hello_world_response(client_socket) do
# Simple HTML content for the response.
content = "<h1>Hello, World!</h1>"
# Generate the entire raw HTTP response, which includes
# calculating content-length header.
response = """
HTTP/1.1 200 OK
content-length: #{byte_size(content)}
content-type: text/html
#{content}
"""
:gen_tcp.send(client_socket, response)
end
end
SimpleServer.start(8080)
We can now start the script in one terminal and probe it with curl
in another to see HTML being returned:
$ elixir simple_server.exs | $ curl http://localhost:8080
Listening on port 8080 | <h1>Hello, World!</h1>%
This works, if by "works" we mean it technically returns HTML.
Is this actually useful? No, not in any practical sense, but it gives a small hint at what happens underneath all the abstractions that the libraries we'll explore today provides.
ℹ️ BTW the full code to this section is here.
Cowboy
Cowboy is a minimalist and popular HTTP server, written in Erlang and used in many Elixir projects.
To try Cowboy, we scaffold an Elixir project with mix
:
$ mix new cowboy_example
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/cowboy_example.ex
* creating test
* creating test/test_helper.exs
* creating test/cowboy_example_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd cowboy_example
mix test
Run "mix help" for more commands.
Then we add Cowboy as a dependency and install dependencies:
$ cd cowboy_example
$ git-nice-diff -U1
cowboy_example/mix.exs
L#23:
[
+ {:cowboy, "~> 2.11"}
# {:dep_from_hexpm, "~> 0.3.0"},
$ mix deps.get
Resolving Hex dependencies...
Resolution completed in 0.043s
New:
cowboy 2.12.0
cowlib 2.13.0
ranch 1.8.0
* Getting cowboy (Hex package)
* Getting cowlib (Hex package)
* Getting ranch (Hex package)
You have added/upgraded packages you could sponsor, run
`mix hex.sponsor` to learn more
ℹ️ BTW
git-nice-diff
is just a small script that works likegit diff
but simplifies the output to make it easier to show diffs in this article. You can find it here if you're curious.
And then we use Cowboy in a module:
defmodule CowboyExample do
def start_server do
# Set up the routing table for the Cowboy server, so root
#requests ("/") direct to our handler.
dispatch = :cowboy_router.compile([{:_, [
{"/", CowboyExample.HelloWorldHandler, []}
]}])
# Start the Cowboy server in "clear mode" aka plain HTTP
# options - Configuration options for the server itself
# (this also supports which IP to bind to,
# SSL details, etc.)
# `env` - Configuration map for how the server
# handles HTTP requests
# (this also allows configuring timeouts,
# compression settings, etc.)
{:ok, _} =
:cowboy.start_clear(
:my_name,
[{:port, 8080}],
%{env: %{dispatch: dispatch}}
)
IO.puts("Cowboy server started on port 8080")
end
end
defmodule CowboyExample.HelloWorldHandler do
# `init/2` is the entry point for handling a new HTTP request
# in Cowboy
def init(req, _opts) do
req = :cowboy_req.reply(200, %{
"content-type" => "text/html"
}, "<h1>Hello World!</h1>", req)
# Return `{:ok, req, state}` where `state` is
# handler-specific state data; here, it's `:nostate`
# as we do not maintain any state between requests.
{:ok, req, :nostate}
end
end
And now we can get an HTML response from the server:
$ iex -S mix | $ curl http://localhost:8080
Generated cowboy_example app. | <h1>Hello World!</h1>%
Erlang/OTP 26 [erts-14.2.2] [ |
source] [64-bit] [smp:12:12] |
[ds:12:12:10] [async-threads: |
1] [dtrace] |
|
Interactive Elixir (1.16.1) - |
press Ctrl+C to exit (type h |
() ENTER for help) |
iex(1)> CowboyExample.start_s |
erver |
Cowboy server started on port |
8080 |
:ok |
iex(2)> |
We have moved up an abstraction level compared to before, with Cowboy now handling the sockets. The code to make Cowboy work is perhaps a bit low-level in how we end up writing raw HTML responses code, and how we have to learn some Erlang-specific syntax and patterns. But it works, which is certainly pleasing.
ℹ️ BTW the full code to this section is here.
Plugged Cowboy
Actually, Cowboy is most commonly used with Plug, which is an Elixir library to make it easy to write HTML-responding functions. In fact, the two are put together so often there's a library just for that purpose: plug_cowboy, which combines both Cowboy and Plug dependencies.
To try it out we generate a new project, but this time we'll also generate a supervisor (the --sup
flag) because Plug manages Cowboy servers in sub-processes:
$ mix new --sup plugged_cowboy
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/plugged_cowboy.ex
* creating lib/plugged_cowboy/application.ex
* creating test
* creating test/test_helper.exs
* creating test/plugged_cowboy_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd plugged_cowboy
mix test
Run "mix help" for more commands.
Then, add the dependency:
$ cd plugged_cowboy
$ git-nice-diff -U1
/plugged_cowboy/mix.exs
L#24:
[
+ {:plug_cowboy, "~> 2.0"}
# {:dep_from_hexpm, "~> 0.3.0"},
$ mix deps.get
Resolving Hex dependencies...
Resolution completed in 0.104s
New:
cowboy 2.10.0
cowboy_telemetry 0.4.0
cowlib 2.12.1
mime 2.0.5
plug 1.15.3
plug_cowboy 2.7.0
plug_crypto 2.0.0
ranch 1.8.0
telemetry 1.2.1
* Getting plug_cowboy (Hex package)
* Getting cowboy (Hex package)
* Getting cowboy_telemetry (Hex package)
* Getting plug (Hex package)
* Getting mime (Hex package)
* Getting plug_crypto (Hex package)
* Getting telemetry (Hex package)
* Getting cowlib (Hex package)
* Getting ranch (Hex package)
Write a request-handling plug:
defmodule PluggedCowboy.MyPlug do
import Plug.Conn
# Plugs must define `init/1`, but we have nothing to configure so it's just a no-op implementation
def init(options), do: options
# `call/2` is the main function of a Plug, and is expected to process the request and generate a response
def call(conn, _options) do
conn
# Both functions below are part of `Plug.Conn`s functions, they're available because we imported it
|> put_resp_content_type("text/plain")
|> send_resp(200, "Hello, World!")
end
end
Configure and register a Plug.Cowboy
child process, which will take care of the underlying Cowboy server:
$ git-nice-diff -U1
/plugged_cowboy/lib/plugged_cowboy/application.ex
L#10:
children = [
+ # Connect Plug.Cowboy plug handler
+ {Plug.Cowboy, plug: PluggedCowboy.MyPlug, scheme: :http, options: [port: 8080]}
# Starts a worker by calling: PluggedCowboy.Worker.start_link(arg)
And… that all just works:
$ iex -S mix | $ curl http://localhost:8080
Erlang/OTP 26 [erts-14.2.2] [ | Hello, World!%
source] [64-bit] [smp:12:12] |
[ds:12:12:10] [async-threads: |
1] [dtrace] |
Interactive Elixir (1.16.1) - |
press Ctrl+C to exit (type h |
() ENTER for help) |
iex(1)> |
Plug definitely offers us several useful simplifications, by bringing our code into pure Elixir and abstracting over both the Cowboy server details and the raw HTML details in the response function.
ℹ️ BTW the full code to this section is here.
Bandit
Bandit is a modern Elixir web server that integrates well with Elixir's concurrency model, offering great performance.
ℹ️ BTW there's humor in the naming: Bandit is an alternative to Cowboy, and internally Cowboy uses a dependency called Ranch to orchestrate sockets, and Bandit decided to call their socket-wrangling dependency Thousand Island 😂.
Let's generate a new project and add dependencies:
$ mix new --sup bandit_example > /dev/null
$ cd bandit_example
$ git-nice-diff -U1
/bandit_example/mix.exs
L#24:
[
+ {:bandit, "~> 1.0"},
+ {:plug, "~> 1.0"}
# {:dep_from_hexpm, "~> 0.3.0"},
$ mix deps.get
ℹ️ BTW the
> /dev/null
just silences themix new
command.
Then write the code:
$ git-nice-diff -U1
/bandit_example/lib/bandit_example/application.ex
L#10:
children = [
+ {Bandit, plug: BanditExample.MyPlug, port: 8080}
# Starts a worker by calling: BanditExample.Worker.start_link(arg)
/bandit_example/lib/bandit_example/my_plug.ex
L#1:
+defmodule BanditExample.MyPlug do
+ import Plug.Conn
+
+ def init(options), do: options
+
+ def call(conn, _options) do
+ conn
+ |> put_resp_content_type("text/plain")
+ |> send_resp(200, "Hello, World!")
+ end
+end
And that just works:
$ iex -S mix | $ curl http://localhost:8080
| Hello, World!%
Erlang/OTP 26 [erts-14.2.2] [ |
source] [64-bit] [smp:12:12] |
[ds:12:12:10] [async-threads: |
1] [dtrace] |
|
Interactive Elixir (1.16.1) - |
press Ctrl+C to exit (type h |
() ENTER for help) |
iex(1)> |
This turned out very similar to Plugged Cowboy, and that's no accident: Bandit is designed as a drop-in replacement to Cowboy. So, since this wasn't a challenge, how about we move ahead and connect to our database?
Connecting To Database
Add the Postgres adapter Postgrex
:
$ git-nice-diff -U1
bandit_example/mix.exs
L#25:
{:bandit, "~> 1.0"},
- {:plug, "~> 1.0"}
+ {:plug, "~> 1.0"},
+ {:postgrex, ">= 0.0.0"}
# {:dep_from_hexpm, "~> 0.3.0"},
$ mix deps.get > /dev/null
Initialize the database:
$ mkdir -p priv/db
$ initdb -D priv/db
$ pg_ctl -D priv/db -l logfile start
We also need a user and a database:
$ createuser -d bandit
$ createdb -O bandit bandit
And register the Postgrex process in our application's supervisor tree:
$ git-nice-diff -U1
bandit_example/lib/application.ex
L#10:
children = [
- {Bandit, plug: BanditExample.MyPlug, port: 8080}
+ {Bandit, plug: BanditExample.MyPlug, port: 8080},
+ {Postgrex,
+ [
+ name: :bandit_db,
+ hostname: "localhost",
+ username: "bandit",
+ password: "bandit",
+ database: "bandit"
+ ]
+ }
# Starts a worker by calling: BanditExample.Worker.start_link(arg)
Now we can add a database query to our HTML response:
$ git-nice-diff -U1
bandit_example/lib/my_plug.ex
L#6:
def call(conn, _options) do
+ %Postgrex.Result{rows: current_time} =
+ Postgrex.query(:bandit_db, "SELECT NOW() as current_time", [])
+
conn
|> put_resp_content_type("text/plain")
- |> send_resp(200, "Hello, World!")
+ |> send_resp(200, "Hello, World! It's #{current_time}")
end
The response we get reflects the database query the server performs when generating the response:
20:04:22.598 [info] Running B | $ curl http://localhost:8080
anditExample.MyPlug with Band | Hello, World! It's 2024-03-17
it 1.3.0 at 0.0.0.0:8080 (htt | 19:04:24.547992Z%
p) |
Erlang/OTP 26 [erts-14.2.2] [ |
source] [64-bit] [smp:12:12] |
[ds:12:12:10] [async-threads: |
1] [dtrace] |
|
Interactive Elixir (1.16.1) - |
press Ctrl+C to exit (type h |
() ENTER for help) |
iex(1)> |
We've climbed up from brutal raw socket handling to using powerful high-level web- and SQL-abstractions, so we can once again claim everything works 🎉
ℹ️ BTW the full code to this section is here.
Phoenix Framework
We can't skip Phoenix, the dominant framework for Elixir web development. Phoenix is a powerful, flexible, and highly ergonomic framework for writing very scalable web applications.
Generate and bootstrap a Phoenix sample app:
$ mix local.hex --force && mix archive.install hex phx_new --force
$ mix phx.new my_app # answer yes when prompted
$ cd my_app
$ mkdir -p priv/db && initdb -D priv/db && pg_ctl -D priv/db -l logfile start && createuser -d postgres
$ mix deps.get && mix ecto.create
ℹ️ BTW if you get database errors, you might need to reset Postgres:
$ lsof -ti :5432 | xargs -I {} kill {}; rm -rf priv/db # Kill all processes on Postgres' default port $ rm -rf priv/db # Delete the local DB data folder
Now start the app with iex -S mix phx.server
and visit http://localhost:4000
:
What Phoenix Gives Us
The Phoenix generator connects to Postgres using Ecto
which is a library designed to efficiently create database queries.
And Ecto actually uses Postgrex under the hood; if we read config/dev.exs
we see the same pattern of hardcoded database credentials that we saw in the Bandit section:
$ cat config/dev.exs
…
# Configure your database
config :my_app, MyApp.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
database: "my_app_dev",
…
Let's also make our Phoenix example use the database:
$ git-nice-diff -U1
my_app/lib/my_app/query.ex
L#1:
+defmodule MyApp.Query do
+ import Ecto.Query
+
+ alias MyApp.Repo
+
+ def get_db_time do
+ # The SELECT 1 is a dummy table to perform a query without a table
+ query = from u in fragment("SELECT 1"), select: fragment("NOW()")
+ query |> Repo.all() |> List.first()
+ end
+end
my_app/lib/my_app_web/controllers/page_controller.ex
L#6:
# so skip the default app layout.
- render(conn, :home, layout: false)
+ db_time = MyApp.Query.get_db_time()
+ render(conn, :home, layout: false, db_time: db_time)
end
my_app/lib/my_app_web/controllers/page_html/home.html.heex
L#42:
<div class="mx-auto max-w-xl lg:mx-0">
+ <h1 class="text-lg">Database Time: <%= @db_time %></h1>
<svg viewBox="0 0 71 48" class="h-12" aria-hidden="true">
We now display the database query result on the page:
.
ℹ️ BTW the full code to this section is here.
But there is much more to Phoenix than just getting it working: Phoenix comes with a lot of powerful tooling to create advanced live-updating server-side rendered pages that match or exceed the experience of fully client-side rendered web apps. But it doesn't seem effective to dive into those things here as Phoenix has many amazing guides that introduce its capabilities better than what this article can.
Conclusion
It's been a lot of fun trying out various solutions, but there's no escaping Phoenix ends up as the most realistic choice for our needs: It's a simple and supremely scalable framework with impressive industry-leading features, and it comes with really great documentation and lots of articles and forum posts to draw inspiration from.
That's not to discount the alternatives, e.g. Bandit: For simple or very specialized needs there wouldn't be anything wrong with choosing Bandit, as it does a great job at providing the essential web server. That is, after all, why Phoenix uses Bandit, and it is delightful how these libraries manages to interact and support each other via Elixir's mature and welcoming community.
Phoenix is the obvious fit for this article series , and I will continue to use it in future articles.
Top comments (0)