Phoenix contexts are simpler than you think

“There's some things we need to put into context…”

A lot of Phoenix beginners get confused by contexts. They’re not sure what contexts are, how to use them, or what their contexts should be called.

In this post I’ll explain contexts as simply as possible. It shouldn’t be difficult, because contexts are, in fact, very simple! The confusion, I think, comes from people overthinking things, and trying to understand contexts as something more complicated than they actually are.

So here’s the explanation: contexts are Elixir modules. They’re collections of functions that you put in the same place (i.e. in the same module) because they’re conceptually related.

That’s it! It’s not much different from how you’d organize your code in any project: related functionality gets grouped together, in the same file.

To get the full picture, let’s talk about how Phoenix codebases are organized:

App vs Web directories

In a Phoenix codebase, like in any project built with Mix (Elixir’s build tool), the bulk of your code lives in the lib directory. In Phoenix specifically, lib is divided into two “buckets”: lib/<app_name> and lib/<app_name>_web.

For example, in Learn Phoenix LiveView, you use Phoenix to build a Slack clone called Slax. When you generate the initial codebase using mix phx.new slax, the new lib directory contains two subdirectories slax and slax_web:

lib
├── slax
└── slax_web

lib/slax contains the app’s core business logic, such as code that queries and updates the database. lib/slax_web handles only the things that are specifically related to serving the app over the web, such as routing HTTP requests and rendering HTML.

This is a logical way to divide things. There’s a pretty clear distinction between the “general” and “web only” parts of a web app.

Think, for example, about sending a message to a Slax channel. The “core business logic” might look like this:

  • Check user permission (e.g. that they’re allowed to post in this channel.)
  • Validate message content (e.g. it’s not blank.)
  • Save the new message to the database.
  • Send a “new message” notification (e.g. email, mobile push notifications) to those who are subscribed to them.
  • Write to the app’s logs.

Note that none of these steps are specific to a web app. We’d still do them even if Slax was, say, a mobile app or a command-line interface. So the code for this logic lives in lib/slax.

For a web-based Slax, however, certain logic is web-specific. For example, we need to:

  • Parse incoming HTTP requests and their parameters.
  • Authenticate the user from their cookies.
  • Return HTTP responses and render HTML.
  • Open websockets and send messages along them.

This “web only” logic lives in lib/slax_web, and it’s mostly handled by Phoenix constructs such as controllers, views, LiveViews and the router.

You’re free to add other directories within lib, and to organize your code in whatever way makes sense for you. But the “core” vs “web” distinction is so fundamental that it was decided to make it the default.

And this is where contexts come in…

Learn LiveView in 15 lessons

Sign up to my newsletter to get instant access to the LiveView and OTP crash course, the FREE tutorial that’ll teach you Phoenix LiveView and OTP in 15 easy lessons.

Your privacy is my top priority. Your email address will never be shared with third parties.

Contexts define the boundaries of your app layer

When you split a codebase in two, the obvious next question is “how do the pieces communicate with each other”?

In Slax, for example, there’s a schema called Room, representing a chat room. (I use the word “Room” for what Slack calls “Channels”, to avoid confusion with the unrelated concept of a Phoenix channel.)

 # lib/slax/chat/room.ex
 defmodule Slax.Chat.Room do
   use Ecto.Schema
   

   schema "rooms" do
     field :name, :string
     field :topic, :string

     has_many :messages, Slax.Chat.Message

     timestamps(type: :utc_datetime)
   end

   
 end

Suppose we need a controller in lib/slax_web that returns a list of all chat rooms. The simple approach would be to load the rooms directly using Repo:

 # lib/slax_web/controllers/room_controller.ex
 defmodule SlaxWeb.RoomController do
   use SlaxWeb, :controller

   alias Slax.Repo
   alias Slax.Chat.Room

   import Ecto.Query

   def index(conn, _params) do
     rooms = Repo.all(from r in Room, order_by: [asc: r.name])

     render(:index, rooms: rooms)
   end
 end

This works, but it’s arguably not good design. Repo provides direct access to the database. By calling Repo’s functions directly from RoomController, it implies that the controller can call any other function on Repo too. That would mean the controller can do anything with the app’s data: it could read, update or delete any record, even those that have nothing to do with rooms. No boundaries are enforced.

Repo belongs in the app layer, in lib/slax. It’s a low-level module that we use to define core business logic around reading and updating data. We shouldn’t use Repo in lib/slax_web. Instead, we expose the specific functionality we want in a context module:

 # lib/slax/chat.ex
 defmodule Slax.Chat do
   alias Slax.Repo
   alias Slax.Chat.Room

   import Ecto.Query

   def list_rooms do
     Repo.all(from r in Room, order_by: [asc: r.name])
   end
 end

Then we call this function from the controller:

 # lib/slax_web/controllers/room_controller.ex
 defmodule SlaxWeb.RoomController do
   use SlaxWeb, :controller

   alias Slax.Chat

   def index(conn, _params) do
     rooms = Chat.list_rooms()

     render(:index, rooms: rooms)
   end
 end

From the web layer’s (i.e. the controller’s) point of view, it just wants to “list all rooms”. It doesn’t care how: the rooms could be loaded from the database using Repo, they could be pulled from an external API, or they could be fed into the computer by a team of trained chimpanzees. It’s not the web layer’s job to think about what “loading a room” means on a technical level. It just asks the app layer for what it needs, and the app layer handles the details.

By preventing controllers (or anything else in lib/slax_web, such as LiveViews) from using Repo directly, we’re making our design cleaner. Rather than an open-ended approach where anything is possible, we’ve clearly defined exactly what chat-related behavior is available (currently just one simple function, but we’ll add more) and this can be customized to the exact requirements of our business logic.

We didn’t have to call the context Chat. Context names don’t have to match the names of your Ecto schemas, controllers, LiveViews, or anything else. You can name them anything you like. The point is to identify logical ways to organize your app’s behavior at a conceptual level.

For example, the Phoenix docs describe an ecommerce app with contexts called Catalog, ShoppingCart and Orders, each of which provides functions that are specific to that particular aspect of the app’s functionality and which deal with multiple database tables and schemas (e.g. within the Catalog context are schemas Catalog.Product and Catalog.ProductCategory.)

To reiterate, contexts are just regular Elixir modules containing normal functions. They often talk to a database or make calls to external services and APIs, but you can put anything within them and name them however you like. They’re yours to tailor to the precise requirements of your app.

Despite contexts’ simplicity, they can be one of the harder things for beginners to get used to — perhaps because they’re so flexible. It’s a contrast to the “opinionated” style of something like Ruby on Rails, where there’s usually a single conventional “Rails way” to guide your decisions.

I hope I’ve managed to clear things up. Just remember the key rule: don’t overthink it. When building a new Phoenix app, don’t worry too much about picking perfect names for your contexts. It’s fine, for example, to re-use the same name as your schemas, e.g. a Rooms context to manage Room records. As your app grows, you can decompose and re-organize your contexts as fits your requirements.

Learn LiveView in 15 lessons

Sign up to my newsletter to get instant access to the LiveView and OTP crash course, the FREE tutorial that’ll teach you Phoenix LiveView and OTP in 15 easy lessons.

Your privacy is my top priority. Your email address will never be shared with third parties.