DEV Community

Cover image for A First Look at the Phoenix Framework
Rafael Andrade
Rafael Andrade

Posted on

A First Look at the Phoenix Framework

As a .NET developer embarking on a journey to learn Elixir, one of the first questions is: "How do I build a web API?" In the Elixir ecosystem, the answer is overwhelmingly the Phoenix Framework. If you're familiar with ASP.NET Core, you'll find Phoenix to be a powerful and elegant counterpart.

In this article, I'll break down the key components of Phoenix and draw comparisons to the .NET world to help frame my understanding.

Overview

Phoenix is the web development framework for the Elixir language. It allows you to build modern web applications, including Web APIs, Web Sockets (for real-time functionality), and traditional Model View Controller (MVC) apps.

In the Microsoft world, it is the direct equivalent of ASP.NET Core. Phoenix is known for high developer productivity and exceptional application performance.

Plug: The HTTP Abstraction

Just as ASP.NET Core is built on top of the middleware concept, Phoenix is built on Plug.

Plug is both a specification for composable modules between web applications and an abstraction layer for web servers (like Cowboy or Bandit. The core concept is a unified connection (the %Plug.Conn{} struct, similar to HttpContext in .NET) that is transformed as it moves through a series of functions.

This is more granular than ASP.NET Core's middleware. A single Plug is a function that takes a connection and returns a connection, making them highly composable.

Pipelines: Organized Middleware

In Phoenix, you group Plugs into "pipelines" within your router. This is a cleaner and more declarative way to organize middleware compared to the app.UseMiddleware<T>() calls in the Program.cs file.

In Phoenix, we explicitly create a pipeline. You add your middleware (called "plugs")—such as authentication or validation—explicitly before the request hits your controller.

Phoenix Example:

defmodule HelloWeb.Router do
  use HelloWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {HelloWeb.LayoutView, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    plug HelloWeb.Plugs.Locale, "en"
  end

  scope "/", HelloWeb do
    pipe_through :browser

    get "/", PageController, :index
  end
Enter fullscreen mode Exit fullscreen mode

In the sample above, we created a pipeline named :browser. Before a request can hit the PageController, it must pass through this pipeline. This feels much clearer than .NET because you can see, in a single place, exactly what steps a request goes through before reaching the controller.

Simplifying Controllers with Plugs

Plugs can also be used to clean up your controller logic. Look at this "messy" controller:

defmodule HelloWeb.MessageController do
  use HelloWeb, :controller

  def show(conn, params) do
    case Authenticator.find_user(conn) do
      {:ok, user} ->
        case find_message(params["id"]) do
          nil ->
            conn |> put_flash(:info, "That message wasn't found") |> redirect(to: ~p"/")
          message ->
            if Authorizer.can_access?(user, message) do
              render(conn, :show, page: message)
            else
              conn |> put_flash(:info, "You can't access that page") |> redirect(to: ~p"/")
            end
        end
      :error ->
        conn |> put_flash(:info, "You must be logged in") |> redirect(to: ~p"/")
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

The nesting makes the application flow difficult to follow. We can use Plug to refactor this into a functional pipeline:

defmodule HelloWeb.MessageController do
  use HelloWeb, :controller

  plug :authenticate
  plug :fetch_message
  plug :authorize_message

  def show(conn, params) do
    render(conn, :show, page: conn.assigns[:message])
  end

  defp authenticate(conn, _) do
    case Authenticator.find_user(conn) do
      {:ok, user} ->
        assign(conn, :user, user)
      :error ->
        conn |> put_flash(:info, "You must be logged in") |> redirect(to: ~p"/") |> halt()
    end
  end

  defp fetch_message(conn, _) do
    case find_message(conn.params["id"]) do
      nil ->
        conn |> put_flash(:info, "That message wasn't found") |> redirect(to: ~p"/") |> halt()
      message ->
        assign(conn, :message, message)
    end
  end

  defp authorize_message(conn, _) do
    if Authorizer.can_access?(conn.assigns[:user], conn.assigns[:message]) do
      conn
    else
      conn |> put_flash(:info, "You can't access that page") |> redirect(to: ~p"/") |> halt()
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

This makes our show method drastically clearer.

Routing

Phoenix routing feels very similar to "Minimal APIs" in .NET. You have a single file defining all routes, pointing them to the specific controller and pipeline that should be used.

defmodule HelloWeb.Router do
  use HelloWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {HelloWeb.Layouts, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

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

  scope "/", HelloWeb do
    pipe_through :browser

    get "/", PageController, :home
  end

  # Other scopes may use custom stacks.
  # scope "/api", HelloWeb do
  #   pipe_through :api
  # end

  # ...
end
Enter fullscreen mode Exit fullscreen mode

Another excellent feature of Phoenix is the ability to list every route in the application via the CLI:

mix phx.routes
Enter fullscreen mode Exit fullscreen mode

Output:

GET  /  HelloWeb.PageController :home
Enter fullscreen mode Exit fullscreen mode

Controllers

Creating a controller in Phoenix is straightforward. You use the base module to bring in the necessary functionality and define actions as functions.

defmodule HelloWeb.PageController do
  use HelloWeb, :controller

  def home(conn, _params) do
    render(conn, :home)
  end
end
Enter fullscreen mode Exit fullscreen mode

Each action takes a connection (conn) and parameters, and returns a (potentially modified) connection. This functional approach—transforming the connection through a series of steps—is a core pattern.

My Opinion: .NET vs. Elixir Ecosystems

Coming from the .NET world, I find myself constantly comparing the two.

It is refreshing that Elixir has one strong, dominant web framework. In other ecosystems, you might have to learn 2 or 3 different frameworks before deciding which to use. In Elixir, the whole community rallies around Phoenix.

Phoenix seems to shine brightest when we talk about real-time features (like LiveView). In .NET, we have SignalR and Blazor Server, but I don't see as many projects adopting them for the full stack compared to Phoenix adoption in Elixir.

The Ecosystem Factor .NET has a massive company behind it. This is both good and bad.

  • The Good: They have immense resources to invest in the ecosystem.
  • The Bad: It can stifle open-source innovation. For example, ASP.NET Core is maintained by Microsoft, so it is very difficult for a community-driven web framework to compete. Similarly, Entity Framework Core effectively replaced NHibernate, and Microsoft's native Dependency Injection reduced the need for libraries like Autofac or Ninject.

In the Elixir world, there is no single tech giant controlling the direction. The Elixir and Phoenix teams (which are relatively small) are doing an incredible job, and their work supports the open-source initiative rather than dominating it.

Reference

https://www.phoenixframework.org/
https://hexdocs.pm/phoenix/overview.html
https://en.wikipedia.org/wiki/Phoenix_(web_framework)
https://elixirschool.com/en/lessons/misc/plug

Top comments (0)