This post is written as a set of Literate Commits. The goal of this style is to show you how this program came together from beginning to end. Each commit in the project is represented by a section of the article. Click each section’s header to see the commit on Github, or check out the repository and follow along.
Starting at the Beginning
Phoenix ships with quite a few bells and whistles. Whenever you fire up mix phx.new
to create a new web application, forty six files are created and spread across thirty directories!
This can be overwhelming to developers new to Phoenix.
To build a better understanding of the framework and how all of its moving pieces interact, let’s strip Phoenix down to its bare bones. Let’s start from zero and slowly build up to a minimum viable Phoenix application.
Minimum Viable Elixir
Starting at the beginning, we need to recognize that all Phoenix applications are Elixir applications. Our first step in the process of building a minimum viable Phoenix application is really to build a minimum viable Elixir application.
Interestingly, the simplest possible Elixir application is simply an *.ex
file that contains some source code. To set ourselves up for success later, let’s place our code in lib/minimal/application.ex
. We’ll start by simply printing "Hello."
to the console.
IO.puts("Hello.")
Surprisingly, we can execute our newly written Elixir application by compiling it:
➜ elixirc lib/minimal/application.ex
Hello.
This confused me at first, but it was explained to me that in the Elixir world, compilation is also evaluation.
lib/minimal/application.ex
+IO.puts("Hello.")
Generating Artifacts
While our execution-by-compilation works, it’s really nothing more than an on-the-fly evaluation. We’re not generating any compilation artifacts that can be re-used later, or deployed elsewhere.
We can fix that by moving our code into a module. Once we compile our newly modularized application.ex
, a new Elixir.Minimal.Application.beam
file will appear in the root of our project.
We can run our compiled Elixir program by running elixir
in the directory that contains our *.beam
file and specifying an expression to evaluate using the -e
flag:
➜ elixir -e "Minimal.Application.start()"
Hello.
Similarly, we could spin up an interactive shell (iex
) in the same directory and evaluate the expression ourselves:
iex(1)> Minimal.Application.start()
Hello.
.gitignore
+*.beam
.DS_Store
lib/minimal/application.ex
-IO.puts("Hello.")
+defmodule Minimal.Application do
+ def start do
+ IO.puts("Hello.")
+ end
+end
Incorporating Mix
This is great, but manually managing our *.beam
files and bootstrap expressions is a little cumbersome. Not to mention the fact that we haven’t even started working with dependencies yet.
Let’s make our lives easier by incorporating the Mix build tool into our application development process.
We can do that by creating a mix.exs
Elixir script file in the root of our project that defines a module that uses Mix.Project
and describes our application. We write a project/0
callback in our new MixProject
module who’s only requirement is to return our application’s name (:minimal
) and version ("0.1.0"
).
def project do
[
app: :minimal,
version: "0.1.0"
]
end
While Mix only requires that we return the :app
and :version
configuration values, it’s worth taking a look at the other configuration options available to us, especially :elixir
and :start_permanent
, :build_path
, :elixirc_paths
, and others.
Next, we need to specify an application/0
callback in our MixProject
module that tells Mix which module we want to run when our application fires up.
def application do
[
mod: {Minimal.Application, []}
]
end
Here we’re pointing it to the Minimal.Application
module we wrote previously.
During the normal application startup process, Elixir will call the start/2
function of the module we specify with :normal
as the first argument, and whatever we specify ([]
in this case) as the second. With that in mind, let’s modify our Minimal.Application.start/2
function to accept those parameters:
def start(:normal, []) do
IO.puts("Hello.")
{:ok, self()}
end
Notice that we also changed the return value of start/2
to be an :ok
tuple whose second value is a PID. Normally, an application would spin up a supervisor process as its first act of life and return its PID. We’re not doing that yet, so we simply return the current process’ PID.
Once these changes are done, we can run our application with mix
or mix run
, or fire up an interactive Elixir shell with iex -S mix
. No bootstrap expression required!
.gitignore
*.beam
-.DS_Store
+.DS_Store
+/_build/
lib/minimal/application.ex
defmodule Minimal.Application do
- def start do
+ def start(:normal, []) do
IO.puts("Hello.")
+ {:ok, self()}
end
mix.exs
+defmodule Minimal.MixProject do
+ use Mix.Project
+
+ def project do
+ [
+ app: :minimal,
+ version: "0.1.0"
+ ]
+ end
+
+ def application do
+ [
+ mod: {Minimal.Application, []}
+ ]
+ end
+end
Pulling in Dependencies
Now that we’ve built a minimum viable Elixir project, let’s turn our attention to the Phoenix framework. The first thing we need to do to incorporate Phoenix into our Elixir project is to install a few dependencies.
We’ll start by adding a deps
array to the project/0
callback in our mix.exs
file. In deps
we’ll list :phoenix
, :plug_cowboy
, and :jason
as dependencies.
By default, Mix stores downloaded dependencies in the deps/
folder at the root of our project. Let’s be sure to add that folder to our .gitignore
. Once we’ve done that, we can install our dependencies with mix deps.get
.
The reliance on :phoenix
makes sense, but why are we already pulling in :plug_cowboy
and :jason
?
Under the hood, Phoenix uses the Cowboy web server, and Plug to compose functionality on top of our web server. It would make sense that Phoenix relies on :plug_cowboy
to bring these two components into our application. If we try to go on with building our application without installing :plug_cowboy
, we’ll be greeted with the following errors:
** (UndefinedFunctionError) function Plug.Cowboy.child_spec/1 is undefined (module Plug.Cowboy is not available)
Plug.Cowboy.child_spec([scheme: :http, plug: {MinimalWeb.Endpoint, []}
...
Similarly, Phoenix relies on a JSON serialization library to be installed and configured. Without either :jason
or :poison
installed, we’d receive the following warning when trying to run our application:
warning: failed to load Jason for Phoenix JSON encoding
(module Jason is not available).
Ensure Jason exists in your deps in mix.exs,
and you have configured Phoenix to use it for JSON encoding by
verifying the following exists in your config/config.exs:
config :phoenix, :json_library, Jason
Heeding that advice, we’ll install :jason
and add that configuration line to a new file in our project, config/config.exs
.
.gitignore
/_build/
+/deps/
config/config.exs
+use Mix.Config
+
+config :phoenix, :json_library, Jason
mix.exs
app: :minimal,
- version: "0.1.0"
+ version: "0.1.0",
+ deps: [
+ {:jason, "~> 1.0"},
+ {:phoenix, "~> 1.4"},
+ {:plug_cowboy, "~> 2.0"}
+ ]
]
Introducing the Endpoint
Now that we’ve installed our dependencies on the Phoenix framework and the web server it uses under the hood, it’s time to define how that web server incorporates into our application.
We do this by defining an “endpoint”, which is our application’s interface into the underlying HTTP web server, and our clients’ interface into our web application.
Following Phoenix conventions, we define our endpoint by creating a MinimalWeb.Endpoint
module that uses Phoenix.Endpoint
and specifies the :name
of our OTP application (:minimal
):
defmodule MinimalWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :minimal
end
The __using__/1
macro in Phoenix.Endpoint
does quite a bit of heaving lifting. Among many other things, it loads the endpoint’s initial configuration, sets up a plug pipeline using Plug.Builder
, and defines helper functions to describe our endpoint as an OTP process. If you’re curious about how Phoenix works at a low level, start your search here.
Phoenix.Endpoint
uses the value we provide in :otp_app
to look up configuration values for our application. Phoenix will complain if we don’t provide a bare minimum configuration entry for our endpoint, so we’ll add that to our config/config.exs
file:
config :minimal, MinimalWeb.Endpoint, []
But there are a few configuration values we want to pass to our endpoint, like the host and port we want to serve from. These values are usually environment-dependent, so we’ll add a line at the bottom of our config/config.exs
to load another configuration file based on our current environment:
import_config "#{Mix.env()}.exs"
Next, we’ll create a new config/dev.exs
file that specifies the :host
and :port
we’ll serve from during development:
use Mix.Config
config :minimal, MinimalWeb.Endpoint,
url: [host: "localhost"],
http: [port: 4000]
If we were to start our application at this point, we’d still be greeted with Hello.
printed to the console, rather than a running Phoenix server. We still need to incorporate our Phoenix endpoint into our application.
We do this by turning our Minimal.Application
into a proper supervisor and instructing it to load our endpoint as a supervised child:
use Application
def start(:normal, []) do
Supervisor.start_link(
[
MinimalWeb.Endpoint
],
strategy: :one_for_one
)
end
Once we’ve done that, we can fire up our application using mix phx.server
or iex -S mix phx.server
and see that our endpoint is listening on localhost
port 4000
.
Alternatively, if you want to use our old standby of mix run
, either configure Phoenix to serve all endpoints on startup, which is what mix phx.server
does under the hood:
config :phoenix, :serve_endpoints, true
Or configure your application’s endpoint specifically:
config :minimal, MinimalWeb.Endpoint, server: true
config/config.exs
+config :minimal, MinimalWeb.Endpoint, []
+
config :phoenix, :json_library, Jason
+
+import_config "#{Mix.env()}.exs"
config/dev.exs
+use Mix.Config
+
+config :minimal, MinimalWeb.Endpoint,
+ url: [host: "localhost"],
+ http: [port: 4000]
lib/minimal/application.ex
defmodule Minimal.Application do
+ use Application
+
def start(:normal, []) do
- IO.puts("Hello.")
- {:ok, self()}
+ Supervisor.start_link(
+ [
+ MinimalWeb.Endpoint
+ ],
+ strategy: :one_for_one
+ )
end
lib/minimal_web/endpoint.ex
+defmodule MinimalWeb.Endpoint do
+ use Phoenix.Endpoint, otp_app: :minimal
+end
Adding a Route
Our Phoenix endpoint is now listening for inbound HTTP requests, but this doesn’t do us much good if we’re not serving any content!
The first step in serving content from a Phoenix application is to configure our router. A router maps requests sent to a route, or path on your web server, to a specific module and function. That function’s job is to handle the request and return a response.
We can add a route to our application by making a new module, MinimalWeb.Router
, that uses Phoenix.Router
:
defmodule MinimalWeb.Router do
use Phoenix.Router
end
And we can instruct our MinimalWeb.Endpoint
to use our new router:
plug(MinimalWeb.Router)
The Phoenix.Router
module generates a handful of helpful macros, like match
, get
, post
, etc… and configures itself to a module-based plug. This is the reason we can seamlessly incorporate it in our endpoint using the plug
macro.
Now that our router is wired into our endpoint, let’s add a route to our application:
get("/", MinimalWeb.HomeController, :index)
Here we’re instructing Phoenix to send any HTTP GET
requests for /
to the index/2
function in our MinimalWeb.HomeController
“controller” module.
Our MinimalWeb.HomeController
module needs to use
Phoenix.Controller
and provide our MinimalWeb
module as a :namespace
configuration option:
defmodule MinimalWeb.HomeController do
use Phoenix.Controller, namespace: MinimalWeb
end
Phoenix.Controller
, like Phoenix.Endpoint
and Phoenix.Router
does quite a bit. It establishes itself as a plug and by using Phoenix.Controller.Pipeline
, and it uses the :namespace
module we provide to do some automatic layout and view module detection.
Because our controller module is essentially a glorified plug, we can expect Phoenix to pass conn
as the first argument to our specified controller function, and any user-provided parameters as the second argument. Just like any other plug’s call/2
function, our index/2
should return our (potentially modified) conn
:
def index(conn, _params) do
conn
end
But returning an unmodified conn
like this is essentially a no-op.
Let’s spice things up a bit and return a simple HTML response to the requester. The simplest way of doing that is to use Phoenix’s built-in Phoenix.Controller.html/2
function, which takes our conn
as its first argument, and the HTML we want to send back to the client as the second:
Phoenix.Controller.html(conn, """
Hello.
""")
If we dig into html/2
, we’ll find that it’s using Plug’s built-in Plug.Conn.send_resp/3
function:
Plug.Conn.send_resp(conn, 200, """
Hello.
""")
And ultimately send_resp/3
is just modifying our conn
structure directly:
%{
conn
| status: 200,
resp_body: """
Hello.
""",
state: :set
}
These three expressions are identical, and we can use whichever one we choose to return our HTML fragment from our controller. For now, we’ll follow best practices and stick with Phoenix’s html/2
helper function.
lib/minimal_web/controllers/home_controller.ex
+defmodule MinimalWeb.HomeController do
+ use Phoenix.Controller, namespace: MinimalWeb
+
+ def index(conn, _params) do
+ Phoenix.Controller.html(conn, """
+ Hello.
+ """)
+ end
+end
lib/minimal_web/endpoint.ex
use Phoenix.Endpoint, otp_app: :minimal
+
+ plug(MinimalWeb.Router)
end
lib/minimal_web/router.ex
+defmodule MinimalWeb.Router do
+ use Phoenix.Router
+
+ get("/", MinimalWeb.HomeController, :index)
+end
Handling Errors
Our Phoenix-based web application is now successfully serving content from the /
route. If we navigate to http://localhost:4000/
, we’ll be greeted by our friendly HomeController
:
But behind the scenes, we’re having issues. Our browser automatically requests the /facicon.ico
asset from our server, and having no idea how to respond to a request for an asset that doesn’t exist, Phoenix kills the request process and automatically returns a 500
HTTP status code.
We need a way of handing requests for missing content.
Thankfully, the stack trace Phoenix gave us when it killed the request process gives us a hint for how to do this:
Request: GET /favicon.ico
** (exit) an exception was raised:
** (UndefinedFunctionError) function MinimalWeb.ErrorView.render/2 is undefined (module MinimalWeb.ErrorView is not available)
MinimalWeb.ErrorView.render("404.html", %{conn: ...
Phoenix is attempting to call MinimalWeb.ErrorView.render/2
with "404.html"
as the first argument and our request’s conn
as the second, and is finding that the module and function don’t exist.
Let’s fix that:
defmodule MinimalWeb.ErrorView do
def render("404.html", _assigns) do
"Not Found"
end
end
Our render/2
function is a view, not a controller, so we just have to return the content we want to render in our response, not the conn
itself. That said, the distinctions between views and controllers may be outside the scope of building a “minimum viable Phoenix application,” so we’ll skim over that for now.
Be sure to read move about the ErrorView
module, and how it incorporates into our application’s endpoint. Also note that the module called to render errors is customizable through the :render_errors
configuration option.
lib/minimal_web/views/error_view.ex
+defmodule MinimalWeb.ErrorView do
+ def render("404.html", _assigns) do
+ "Not Found"
+ end
+end
Final Thoughts
So there we have it. A “minimum viable” Phoenix application. It’s probably worth pointing out that we’re using the phrase “minimum viable” loosely here. I’m sure there are people who can come up with more “minimal” Phoenix applications. Similarly, I’m sure there are concepts and tools that I left out, like views and templates, that would cause people to argue that this example is too minimal.
The idea was to explore the Phoenix framework from the ground up, building each of the requisite components ourselves, without relying on automatically generated boilerplate. I’d like to think we accomplished that goal.
I’ve certainly learned a thing or two!
If there’s one thing I’ve taken away from this process, it’s that there is no magic behind Phoenix. Everything it’s doing can be understood with a little familiarity with the Phoenix codebase, a healthy understanding of Elixir metaprogramming, and a little knowledge about Plug.