F5.01 · part 2 of 3
A thin web server in Elixir
The server that makes the Portal runnable is small enough to read in one screen: a
Plug.Router matched and dispatched by Bandit, a pure-Elixir HTTP
server, added as one child in the supervision tree. Each route does exactly one thing — turn the request
into a command or a query, call Portal.Engine, and send back the result. There is no
framework here and no business logic; the router knows how to speak HTTP and nothing about the Portal’s
rules.
Follow a request through the four steps of the server, then read the whole thing in code. You can
curl it on day one.
A request, four steps
Bandit accepts the connection and hands a conn to the router; the router matches a
handler; the handler calls the engine; the result is encoded and sent. Only the third step is real work —
the rest is plumbing the language and the libraries already provide. Select a step to see what it does.
The request path · select a step
step: Portal.Engine
does: calls the engine
A thin server is mostly other people’s code: Bandit speaks HTTP, Plug routes, Jason encodes. Your part is the one line in the middle that calls the Portal.
The whole server
The router is the entire web layer. A get reads through a query; a
post writes through a command and maps the engine’s tagged result onto a status
code; an unmatched request is a 404. Adding it to the supervision tree as a Bandit
child is the last step — now the Portal is on a port.
defmodule Portal.Web.Router do use Plug.Router plug :match plug Plug.Parsers, parsers: [:urlencoded, :json], json_decoder: Jason plug :dispatch # read: a query, no side effects get "/courses/:user_id" do {:ok, courses} = Portal.Engine.query(:courses_of, user_id) send_resp(conn, 200, Jason.encode!(courses)) end # write: a command, mapped onto a status code post "/enroll" do cmd = %Portal.Commands.EnrollLearner{ user_id: conn.params["user"], course_id: conn.params["course"] } case Portal.Engine.dispatch(cmd) do {:ok, _event} -> send_resp(conn, 201, "enrolled") {:error, reason} -> send_resp(conn, 422, to_string(reason)) end end match _, do: send_resp(conn, 404, "not found") end # the whole server: one child in the supervision tree children = [ Portal.Engine, {Bandit, plug: Portal.Web.Router, port: 4000} ]
a request
An HTTP verb and a path — POST /enroll.
one engine call
The handler builds a command and calls dispatch/1 — nothing more.
Next: a web layer built for replacement — why this thinness is what lets Phoenix take over in F6.
Booting the Portal as a supervised application
An OTP application is a started, supervised unit of work, not merely a folder of modules. The
entry point is a module that calls use Application and implements
start/2, whose job is to start a supervisor over the application’s children. The
Supervisor and Application guide names the wiring that turns the project into one: the
application/0 callback in mix.exs carries a
mod: entry, {Portal.Application, []}, naming the module to
start and the argument to pass it. With that line in place, mix run, an
iex -S mix session, and a release all boot the same tree the same way.
The children are the Portal.Engine and the
Bandit server from the list above, now started under
Portal.Supervisor with a :one_for_one strategy. The thin
server is a supervised application from the first commit, so a crash in one child restarts that child cleanly
instead of taking the node down.
The boot path · mix.exs to a running tree
# lib/portal/application.ex — the OTP entry point, here from the first commit defmodule Portal.Application do use Application @impl true def start(_type, _args) do children = [ Portal.Engine, # the domain core {Bandit, plug: Portal.Web.Router, port: 4000} # the thin web server ] # one_for_one: a crashed child restarts on its own, not the node opts = [strategy: :one_for_one, name: Portal.Supervisor] Supervisor.start_link(children, opts) end end # mix.exs — application/0 names the module to start with mod: def application do [ extra_applications: [:logger], mod: {Portal.Application, []} # Mix starts this on boot ] end # a single command boots the whole supervised tree: # $ mix run --no-halt Process.whereis(Portal.Supervisor) # => #PID<0.214.0> (the tree is up) Supervisor.which_children(Portal.Supervisor) # => [{Bandit, #PID<0.221.0>, :supervisor, [Bandit]}, # {Portal.Engine, #PID<0.216.0>, :worker, [Portal.Engine]}]
an application
A started, supervised unit — not a loose folder of modules that something else must remember to launch.
one mod: line
mod: {Portal.Application, []} in mix.exs makes every boot start the same supervised tree.
Wired through mod: from the first commit, the Portal boots as a supervised
tree, so a falling child is restarted in place rather than dragging the node down with it.
References
Primary sources for this lesson, and where it connects in the course.
Sources
- Supervisor and Application — Elixir documentation —
use Application, themix.exsmod:entry,start/2, and starting a supervisor withstrategy: :one_for_one.