Pete Corey Writing Work Contact

Minimum Viable Phoenix

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


Surprisingly, we can execute our newly written Elixir application by compiling it:

➜ elixirc lib/minimal/application.ex

This confused me at first, but it was explained to me that in the Elixir world, compilation is also evaluation.



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()"

Similarly, we could spin up an interactive shell (iex) in the same directory and evaluate the expression ourselves:

iex(1)> Minimal.Application.start()




+defmodule Minimal.Application do
+  def start do
+    IO.puts("Hello.")
+  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"

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, []}

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
  {:ok, self()}

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!




 defmodule Minimal.Application do
-  def start do
+  def start(:normal, []) do
+    {:ok, self()}


+defmodule Minimal.MixProject do
+  use Mix.Project
+  def project do
+    [
+      app: :minimal,
+      version: "0.1.0"
+    ]
+  end
+  def application do
+    [
+      mod: {Minimal.Application, []}
+    ]
+  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.




+use Mix.Config
+config :phoenix, :json_library, Jason


   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

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
    strategy: :one_for_one

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 :minimal, MinimalWeb.Endpoint, []
 config :phoenix, :json_library, Jason
+import_config "#{Mix.env()}.exs"


+use Mix.Config
+config :minimal, MinimalWeb.Endpoint,
+  url: [host: "localhost"],
+  http: [port: 4000]


 defmodule Minimal.Application do
+  use Application
   def start(:normal, []) do
-    IO.puts("Hello.")
-    {:ok, self()}
+    Supervisor.start_link(
+      [
+        MinimalWeb.Endpoint
+      ],
+      strategy: :one_for_one
+    )


+defmodule MinimalWeb.Endpoint do
+  use Phoenix.Endpoint, otp_app: :minimal

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

And we can instruct our MinimalWeb.Endpoint to use our new 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

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

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, """



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, """



And ultimately send_resp/3 is just modifying our conn structure directly:

  | status: 200,
    resp_body: """


""", 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.


+defmodule MinimalWeb.HomeController do
+  use Phoenix.Controller, namespace: MinimalWeb
+  def index(conn, _params) do
+    Phoenix.Controller.html(conn, """


+ """) + end +end


   use Phoenix.Endpoint, otp_app: :minimal
+  plug(MinimalWeb.Router)


+defmodule MinimalWeb.Router do
+  use Phoenix.Router
+  get("/", MinimalWeb.HomeController, :index)

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"

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.


+defmodule MinimalWeb.ErrorView do
+  def render("404.html", _assigns) do
+    "Not Found"
+  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.

This article was published on May 20, 2019 under the ElixirLiterate CommitsPhoenix tags. For more articles, visit the archives. Also check out the work I do, and reach out if you’re interested in working together.

– It turns out that the problem of detecting server connectivity is more complicated than it first seems in the current state of Apollo.

– It turns out that the process of turning words into a well-formatted, distributable ebook is much more complicated that it seems. Here's how I managed.