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
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
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
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
Another excellent feature of Phoenix is the ability to list every route in the application via the CLI:
mix phx.routes
Output:
GET / HelloWeb.PageController :home
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
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)