Why is Phoenix a Top-Admired Framework?
My absolute favorite web framework is not even on this list and I'm a little bitter about that but we'll get to that later. The most admired framework is Phoenix at 83.7% of respondents.
Another interesting thing is that the host language of Phoenix is Elixir which is the second most admired language in the same survey, second only to Rust of course. So what is going on here? Why is Phoenix so admired? Is there a good reason for it or is it irrational?
It's easy to get fear of missing out when you see these sorts of things and you're not working in that framework, so we're going to unpack all that.
Phoenix Live View in Action
This is a very simple web application built using Phoenix, and Phoenix is heavily inspired by Rails. The command line utilities for creating new applications and the scaffolding is very, very similar. Even the template syntax is very, very similar. But this is not just a traditional Phoenix web application, this is a Phoenix Live View web application. And I think Live View is really what makes Phoenix so admired.
When I perform actions here, I can create a new post and I save it, and you can see in Chrome Dev tools on the right, there is nothing going on in terms of HTTP requests. The reason is because all of this is being sent over a persistent WebSocket connection that gets established when I first navigate to this page. So I can go ahead and click into this WebSocket here that was established and I can see all sorts of stuff going on when I click on things here.
This is interesting because the server can actually initiate changes in data that aren't the result of the client taking some action on the page. This can be something like a language model doing inference and we're kind of streaming it back to the client as we get it, or anything of that nature.
Another interesting capability that sort of emerges from this this paradigm is the ability to stream changes to this data to all users of your web application, not just the person that changed it. We're not using that in this particular application, but it's very possible. And the obvious use cases for that are things like chat applications, right?
You can kind of think of this a little bit like having reactivity on the server side instead of the client side like you'd have with React or Svelte or Leptos. That also means that you can create these rich interactive experiences that don't fully reload the page without writing any client side code.
Understanding Phoenix Routes and Elixir Atoms
So let's break this down. Let's look at the code. Like most web applications, we have a list of routes that we establish. Each of these lines establishes a route. The first element being the type of route that it is. Is it a traditional HTTP method or is it a live view? We can see we have one get route that's kind of the default route that was added when I created the application, and then we have the series of live routes.
Followed by the type of route is the URL, which may or may not have path parameters. We can see in this case there is an ID path parameter, in these two cases there is not. And then we have the Elixir module that we want to associate with that live view, in this case for all three of these routes it is post_live.index.
Finally at the end we have an action. An action you can kind of think of like the state that that live view is in. The live view has a set of discrete states, and each state might inject different data into the template. More on that in a minute. But these actions are specified using what are called atoms, and atoms is are one of the unique feature of Elixir. If you see something prefixed with colon, that means it is an atom, and an atom is a constant whose value is its name, if that makes sense.
The way I think of this is if you had an enum where you could arbitrarily create new variants of the enum on the fly through the code whenever you need them, that's kind of what atoms are. So under the hood I believe they're represented by integers. They're represented really efficiently under the hood, they're not strings or anything like that. So that is what atoms are.
A Deep Dive into HEEx Templates
First let's take a look at the template and this template is called index.html.heex. HEEx is the templating language. The syntax for these is very, very similar to the Rails template syntax. We interpolate values using curly braces, and then to refer to data that got injected into the template we use the @ symbol. Each of these tags either has a dot prefix or a colon prefix. The dot prefix indicates that that tag is a function component which is implemented in Elixir somewhere else. You get a lot of these components out of the box like this header component here, this table component. And inside these function components, you might have a tag that's prefixed with a colon. These are kind of like slots.
The header function component has this action slot, and in the template you can specify what goes inside that action slot. That's what that's doing. Inside that action slot there happens to be a link component which has a patch attribute. And the link component is what allows us to redirect the browser to somewhere, another URL, right? If we do patch, it's going to make a request to that URL through the WebSocket. So when the user clicks on new post, it's going to make a request to /post/new over that WebSocket and replace only the part of the page, or only send back the part of the page that changed, not the entire page. If we do navigate, it's going to refresh the entire page. That's the that's the distinction there.
Then we have this table component, which does exactly what it sounds like. It gives us that table that all the posts are enumerated in. It has a row attribute. And we refer to the post data. The post data is in a what's called a stream, which is a mechanism for maintaining a list of many elements such that the entire page doesn't need to be reloaded every time the collection changes. That's what a stream does. We'll see more of that when we look at the Elixir code.
The table function component has col and action and they're exactly what they sound like. col there's a col for each column. Each row is going to represent a post. And inside that row we render the title and body of that post. And then we have the buttons on the right side, the show and edit buttons. The show button actually takes us to a completely different live view, so that's why we have the navigate attribute on the link function component. On the edit button, we have the patch attribute because that's going to pop up a modal on the same page that we're already on, so we only want to send back the part of the page that changed.
And then at the bottom we have this modal component, which is that box that we use to modify or create new posts. And we can see we render it if the current action of the live view is a new or edit item. Index is a case where we just enumerate all the posts. New and edit both involve a modal. So the modal component only shows when it's in the new or edit state or action, and again that's the model that we use to update the post. Live component is a special function component that kind of encapsulates live view functionality in a component. We interpolate all the values that we need.
The Elixir Logic Behind Live View
Now let's look at the Elixir code for this live view. And we can see we have three apply_action functions. By the way, in Elixir defp means a private function, so this function cannot be referenced outside of this module. That's what that means.
Also, this crazy syntax here, socket, it looks like a triangle right now, it's actually a pipe operator and a right angle bracket is what that is. All that does is take the thing on the left and pass it into the function on the right as the first parameter. So effectively this is actually assign(socket, page_title, edit_post) so on and so forth. And assign returns a socket as well so we can kind of chain it together in that way.
assign is the way we populate the data that needs to be interpretated in that template. So we can reference page_title and post inside that template by doing @ and then page_title or @post. And we can see that if we look at the template for the form, we can see those here, post is used here and page_title is used there.
Right, there are three apply_action functions, one for each of these atoms, or one for each of the actions that which essentially represents a state that the live view can be in. There's edit, new, and index. And the only thing that change, well the page title changes obviously, and then in the edit case we actually get the blog post and assign that data to the post variable that gets interpolated into the template. In the case of new, there's we just make an empty post and if we're just listing the post we we don't need that actually. So that's how that works.
handle_info actually handles the case where we've saved, we've edited or we've created a new post. This is a good example of the backend updating a stream, in this case the post stream, and that update getting reflected on the front end despite the user not taking any action in the browser because that's what happens when the user actually saves that post. When the user saves a new post or edits an existing one the browser doesn't just like slap that into the table. What actually happens is the modal closes and the server actually gets that new data and initiates an update to the stream which gets propagated to the front end via that that WebSocket.
You can use the same mechanism, I mentioned this earlier, you can use the same mechanism to potentially update the user's browser with data that came from other users of your web application. That's what gets me excited about live views is the potential for this kind of global state that goes across users.
And then we have handle_event which just handles the case where a post is deleted.
The Power of the BEAM VM and OTP
Now let's talk about BEAM which is the virtual machine that all of this runs on. And it's a virtual machine in the same sense that the JVM is, but it has much different design goals. BEAM is designed for building resilient, scalable, distributed systems. And when I first heard that, I was like, that's what all tech stacks are aiming to do, right? Well, there's some constructs under the hood that are built into the BEAM VM that are more conducive to this sort of thing. It has a concept of processes. And these are not operating system level processes. These are BEAM specific processes that have isolated areas of memory. They have to communicate with each other via passing of messages. If you're familiar with the actor pattern, that's what this is.
The idea behind this is that you never have to use any locks, which effectively eliminates the possibility of things like deadlocks. It achieves all this through something called OTP, which is Open Telecom Platform, and that's a collection of libraries and abstractions for concurrency, networking, messaging, fault tolerance, and so on. Fault tolerance is achieved through something called supervision trees. So you have a process and that kicks off some child processes. If a child process fails, there's a mechanism for automatically restarting that child process. And I think you can do something similar to this using futures and promises, but my understanding is that there's a little more boilerplate to get the automatic restarting to happen. I haven't gone too far down this rabbit hole, but that's kind of the narrative.
It seems like web frameworks in most tech stacks have some kind of resilience narrative. If something goes wrong in a handler function it doesn't just crash right? It allows you to recover in some capacity or return an error code and then continue handling other requests. Yeah, I haven't gone too far down this rabbit hole. I'm still a little bit fuzzy on how it helps in the context of web applications. Let me know down in the comments if you know. I know Elixir experts, Phoenix experts are going to be watching this. I would love to hear the explanation of how supervision trees specifically helps as compared to a similar framework in a different tech stack.
Now let's talk about scalability and Anton Putra, a fantastic YouTuber who publishes very detailed videos on load testing different tech stacks and comparing them, he happened to do a test on Elixir versus Go. Now this does not use Phoenix, this uses something called Bandit which is a simple HTTP framework which actually Phoenix uses under the hood. So I kind of look at this as a theoretical upper bound to the potential performance or load capability, load handling capabilities of Phoenix. That that might not be fair, it's not an apples to apples comparison.
But first of all let me preface this by saying this, the results of this benchmark will be irrelevant to 99.9% of web applications because the differences between Go and Elixir in this context only become apparent at the multi-thousands of requests per second threshold. If you're handling fewer requests than that the differences are not going to be apparent.
What I'm about to say is not a replacement for watching this video, you should go and watch this video, support Anton, he has a fantastic channel. But my takeaway here is that at like 45,000 requests, something like that, the P90 latency of the Elixir application starts to skyrocket while that of the Go application, like it scales linearly with the number of requests per second. Similarly in CPU usage and memory usage we see much higher numbers in the Elixir stack than in the Go stack. So as far as the scalability narrative goes, yes it does scale, but there is a order of magnitude problem after you reach a certain threshold of requests per second with Elixir.
The creator of the Elixir language was actually submitting pull requests to the test code for this particular benchmark. There's still some back and forth between Anton and the creator of Elixir. This is the second attempt, the Elixir creator actually submitted some changes to the test and Anton subsequently ran this. Anyway, I think this is a pretty good starting point for understanding how the Elixir tech stack performs under heavy load. Again, this do not read too much into this. This is not going to be relevant for 99.9% of web apps. Most web apps are going to be just fine with the Elixir stack. The other thing is this is a traditional web app that's being tested here. This is not a live view app. I don't know if the performance characteristics of the Elixir stack are going to be similar given a live view application. I have no idea.
Ideal Use Cases for Phoenix Live View
So is Phoenix deserving of being the most admired web framework? I can see it. I'm glad I learned it. I think it's really interesting. It has some ideas that you don't really see many other places. I would there's definitely a set of use cases that I would feel comfortable using it with. I'm definitely not going to run out and start using it for everything.
The primary things I would want to use it for are situations where again, there we need to share some state across all users of the application like a chat room or something like that. That seems like one of the sweet spots for it. Another is anywhere you'd use websockets for something else. So instead of having this traditional web application that has websockets bolted on top of it just do everything with the websocket. I think that would simplify things potentially. One of the use cases for that is streaming language model responses. Language model inference is a little bit slow. You can't make a request and have the client wait for the entire response. They're going to be waiting a while. They're going to want to see some of those results before the entire thing comes back. That's a prime use case for WebSockets and subsequently something like Phoenix Live Views. If I were making a like a an LLM chatbot I might use Phoenix Live View for that.
Those are some of the use cases I would consider using Phoenix for. If my application was ultra traffic heavy, I probably wouldn't use it just based on Anton's results. Again, doesn't matter for probably anything that I'm going to build. If you like rails, you might like Phoenix irrespective of Live View, but I don't think that's the reason it's the most admired web framework.
In the beginning I mentioned my absolute favorite web framework didn't even get a mention on the Stack Overflow developer survey. In case you're wondering the framework is Leptos and it's written in Rust. It is a great framework in part because it allows you to write all your front end and back code in Rust. If that sounds interesting but you're not familiar with rust yet you might be interested in Ultimate Rust Crash Course which is available on Skillshare, the sponsor of this video. Instructor Nathan Stocks has an engaging style and I love how he makes heavy use of visuals in the course. Skillshare has thousands of classes across a wide variety of topics like productivity. I'm a fan of Ali Abdall by the way, art photography, and UX design. UX design is what I'm personally going to be working on.
Skillshare is the largest online learning community for creatives with thousands of classes led by industry experts across many different topics. It can help you take your career skills, hobbies, passions, or side hustles to the next level. The first 500 people to use my link in the description will receive a one-month free trial of Skillshare. Get started today.
If you already know the basics of rust or you just want to cut to the chase and check out Leptos, check out this video where I build a very simple web application using Leptos and also integrate it with Stripe. Thanks for watching and we'll see you in the next one.