Search⌘ K
AI Features

Phoenix (One Giant Function)

Explore how Phoenix processes web requests through a series of plugs combined into pipelines at the endpoint and router levels. Understand how these pipelines define request policies and how routing directs requests to appropriate modules like LiveView. Learn the foundational approach to building layered authentication using plugs to secure user access within Phoenix applications.

To understand how Phoenix handles web requests, and therefore how LiveView handles web requests, you can think of Phoenix requests as simply one big function broken down into smaller plugs. These plugs are stitched together, one after another, as if they were in one big pipeline.

The main sections of the giant Phoenix pipeline are the endpoint, the router, and the application. You can visualize any Phoenix request with this CRC pipeline:

Elixir
connection_from_request
|> endpoint
|> router
|> custom_application

Each one of these pieces is made up of tiny functions. The custom_application can be a Phoenix controller, a Phoenix channels application, or a LiveView. We’ll spend most of the lessons on LiveView. For now, let’s discuss the first two parts of this pipeline, the endpoint and router.

The Phoenix endpoint

If Phoenix is a long chain of reducer functions called plugs, the endpoint is the constructor at the very beginning of that chain. The endpoint is a simple Elixir module in a file called endpoint.ex, and it has exactly what we would expect—a pipeline of plugs.

We might ever change our endpoint.ex file, so we won’t read through it in detail. Instead, we’ll just scan through it to confirm that every Phoenix request goes through an explicit list of functions called plugs.

To do so, we open up lib/pento_web/endpoint.ex, and we’ll notice that it has a bit of configuration followed by a bunch of plugs.

Elixir
defmodule PentoWeb.Endpoint do
use Phoenix.Endpoint, otp_app: :pento
@session_options [
store: :cookie,
key: "_pento_key",
signing_salt: "GwwWu/ZA"
]
socket "/socket", PentoWeb.UserSocket,
websocket: true,
longpoll: false
socket "/live", Phoenix.LiveView.Socket,
websocket: [connect_info: [session: @session_options]]
plug Plug.Static,
at: "/",
from: :pento,
gzip: false,
only: ~w(css fonts images js favicon.ico robots.txt)
if code_reloading? do
socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
plug Phoenix.LiveReloader
plug Phoenix.CodeReloader
plug Phoenix.Ecto.CheckRepoStatus, otp_app: :pento
end
plug Phoenix.LiveDashboard.RequestLogger,
param_key: "request_logger",
cookie_key: "request_logger"
plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options
plug PentoWeb.Router
end

That configuration defines the socket that will handle the communication for all of our LiveView, but the details are not important right now.

After those sockets, all the way down at the bottom, we see a list of plugs, and every one of them transforms the connection in some small way.

Elixir
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options
plug PentoWeb.Router

You don’t have to know what these plugs do yet. Just know that requests, in the form of Plug.Conn connections, flow through the plugs and eventually reach the Router.

The Phoenix router

Think of a router as a switchboard operator; its job is to route the requests to the bits of code that make up your application. Some of those bits of code are common pieces of policy. A policy defines how a given web request should be treated and handled. For example, browser requests may need to deal with cookies, API requests may need to convert to and from JSON, and so on. The router does its job in three parts.

  • First, the router specifies chains of common functions to implement policy.
  • Next, the router groups together common requests and ties each one to the correct policy.
  • Finally, the router maps individual requests onto the modules that do the hard work of building appropriate responses.

Let’s see how that works. Open up pento/lib/pento_web/router.ex in the Phoenix application code given below.

Elixir
#--
# Copyrights apply to this code. It may not be used to create training material,
# courses, books, articles, and the like
#--
defmodule PentoWeb.Router do
use PentoWeb, :router
import PentoWeb.UserAuth
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {PentoWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :fetch_current_user
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", PentoWeb do
pipe_through :browser
live "/", PageLive, :index
end
# Other scopes may use custom stacks.
# scope "/api", PentoWeb do
# pipe_through :api
# end
# Enables LiveDashboard only for development
#
# If you want to use the LiveDashboard in production, you should put
# it behind authentication and allow only admins to access it.
# If your application does not have an admins-only section yet,
# you can use Plug.BasicAuth to set up some basic authentication
# as long as you are also using SSL (which you should anyway).
if Mix.env() in [:dev, :test] do
import Phoenix.LiveDashboard.Router
scope "/" do
pipe_through :browser
live_dashboard "/dashboard", metrics: PentoWeb.Telemetry
end
end
## Authentication routes
scope "/", PentoWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated]
get "/users/register", UserRegistrationController, :new
post "/users/register", UserRegistrationController, :create
get "/users/log_in", UserSessionController, :new
post "/users/log_in", UserSessionController, :create
get "/users/reset_password", UserResetPasswordController, :new
post "/users/reset_password", UserResetPasswordController, :create
get "/users/reset_password/:token", UserResetPasswordController, :edit
put "/users/reset_password/:token", UserResetPasswordController, :update
end
scope "/", PentoWeb do
pipe_through [:browser, :require_authenticated_user]
live "/guess", WrongLive
get "/users/settings", UserSettingsController, :edit
put "/users/settings/update_password", UserSettingsController, :update_password
put "/users/settings/update_email", UserSettingsController, :update_email
get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
end
scope "/", PentoWeb do
pipe_through [:browser]
delete "/users/log_out", UserSessionController, :delete
get "/users/confirm", UserConfirmationController, :new
post "/users/confirm", UserConfirmationController, :create
get "/users/confirm/:token", UserConfirmationController, :confirm
end
end

You’ll find more plugs and some mappings between specific URLs and the code that implements those pages. Each grouping of plugs provides a policy for one or more routes. Here’s how it works.

How pipelines are policies

A pipeline is a grouping of plugs that applies a set of transformations to a given connection. The set of transformations applied by a given plug represents a policy. Since we know that every plug takes in a connection and returns a connection, we also know that the first plug in a pipeline takes a connection and the last plug in that pipeline returns a connection. So, a plug pipeline works exactly like a single plug! Here’s a peek at what the browser pipeline will look like. The pipeline implements the policy our application needs to process a request from a browser:

Elixir
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {PentoWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :fetch_current_user
end

This bit of code says we’re going to accept only HTML requests, and then we’ll fetch the session, and so on. This api pipeline implements the policy for an API:

Elixir
pipeline :api do
plug :accepts, ["json"]
end

It has a single plug that dictates associated routes will accept only JSON requests.

Now that we know how to build a policy, the last thing we need to do is to tie a particular URL to a policy, and then to the code responsible for responding to the request for the particular URL.

Scopes

A scope block groups together common kinds of requests, possibly with a policy. Here’s a set of common routes in a scope block.

Elixir
scope "/", PentoWeb do
pipe_through :browser
live "/", PageLive, :index
end
if Mix.env() in [:dev, :test] do
import Phoenix.LiveDashboard.Router
scope "/" do
pipe_through :browser
live_dashboard "/dashboard", metrics: PentoWeb.Telemetry
end
end
## Authentication routes
scope "/", PentoWeb do
pipe_through [:browser, :redirect_if_user_is_authenticated]
get "/users/register", UserRegistrationController, :new
post "/users/register", UserRegistrationController, :create
get "/users/log_in", UserSessionController, :new
post "/users/log_in", UserSessionController, :create
get "/users/reset_password", UserResetPasswordController, :new
post "/users/reset_password", UserResetPasswordController, :create
get "/users/reset_password/:token", UserResetPasswordController, :edit
put "/users/reset_password/:token", UserResetPasswordController, :update
end
scope "/", PentoWeb do
pipe_through [:browser, :require_authenticated_user]
live "/guess", WrongLive
get "/users/settings", UserSettingsController, :edit
put "/users/settings/update_password", UserSettingsController, :update_password
put "/users/settings/update_email", UserSettingsController, :update_email
get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
end
scope "/", PentoWeb do
pipe_through [:browser]
delete "/users/log_out", UserSessionController, :delete
get "/users/confirm", UserConfirmationController, :new
post "/users/confirm", UserConfirmationController, :create
get "/users/confirm/:token", UserConfirmationController, :confirm
end

This tiny block of code does a lot. The scope expression means the provided block of routes between the do and the end applies to all routes because all routes begin with /. The pipe_through :browser statement means every matching request in this block will go through all of the plugs in the :browser pipeline. We’ll handle the routes next.

Routes

The last bit of information is the individual routes. Let’s list our route one more time for clarity.

Elixir
live "/", PageLive, :index

Every route starts with a route type, a URL pattern, a module, and options. LiveView routes have the type live.

The URL pattern in a route is a pattern matching statement. The "/" pattern will match the URL /, and a pattern of "/index" will match a URL like /index, and so on.

The next bit of information is the PageLive module, which implements the code that responds to the request. The type of route will determine what kind of code to do this responding to. Since our route is a live route, the PageLive module will implement a LiveView.

The last option is the :index live action. It’s just a bit of metadata about the request. As we go, we’ll offer more information about routes. For now, let’s move on.

Plugs and authentication

Now we need to think about authentication. Web applications almost always need to know who’s logged in. Authentication is the service that answers the question “Who is logging in?”. Only the most basic applications can be secure without authentication.

The authentication service we are going to build will let in only those who have accounts on our game server. Since we plan to have pages only our registered users should see, we will need to secure those pages. We must know who is logging in before we can decide whether or not to let them in.

Now, let’s put all of that information about plugs into action. Let’s discuss a plan for authentication. We will build our authentication system in layers, as demonstrated in this figure.

On the left side is the infrastructure. This will use a variety of services to store long-term user data in the database, short-term session data in cookies, and provide user interfaces to manage user interactions.

On the right side, the Phoenix router will send appropriate requests through authentication plugs within the router, and these plugs will control access to custom LiveView, channels, and controllers.

We’ll go into each of these layers in detail throughout the rest of the lessons. Suffice to say we’re not going to build this service ourselves. Instead, we’ll generate it from an existing dependency. Let’s get to work!