The Parts of Ring

Summary: Ring, the Clojure Web library, defines three main concepts that you use to construct web applications.

Ring is the center of the Clojure web universe. It's not the only option in town, but the other options refer to Ring to say how they are different. Understanding Ring will help you learn any other web system in Clojure.

Ring has three main concepts that work together to build a web application.

  1. Adapters
  2. Handlers
  3. Middleware

Adapters

I like to think of Ring Adapters as plug adapters. When you go to a different continent, you often have to adapt the holes in the electical outlet to fit your cords. Ring Adapters let you convert different web server implementations into a standard Ring Request/Response format. That way, all of your code is standardized to the Ring format. Your code can travel into any kind of server as long as an adapter exists.

There are Ring Adapters for many existing servers.

And more. The right Ring Adapter will let your same application run in any of them.

Handlers

Handlers do the work of your application. They are like the computer. They are just Clojure functions. HTTP is basically a request/response protocol that maps well to functions, which are just a protocol from argument to return value. Handlers take a Ring Request and return a Ring Response. They should do whatever logic is necessary for your application.

Middleware

Middleware are the voltage converters. Here in North America, wall sockets run at 120 volts, which is different from almost everywhere. In order to run an appliance from elsewhere, you not only need to adapt the socket, you also need to transform the current to a compatible voltage. Middleware are often used to convert the incoming request in some standard way. For instance, there is middleware to parse a JSON body into a Clojure map and store it away in the request.

The transformer also "cleans up" the current. Voltage spikes are evened out so they never get to the computer. Middleware can similarly protect a handler by making sure the browser is logged in.

The analogy kind of breaks down, because middleware can do work (like the computer). Middleware are the hardest part of the Ring idea. They're not hard because the concept is hard. They're hard because they require design decisions. If all you had were Adapters and Handlers, you wouldn't have to think about where to put your logic. It would all go in the Handlers.

But there would be a lot of duplicated logic in your handlers. Authentication, routing, content-type switching, all of these things are done the same way over and over. It's the perfect problem for a little higher order programming. That's essentially what Middleware is.

Ring Middleware are functions that take a Handler and return a new Handler. Since Handlers are functions, Middleware are higher-order functions. The transformer on your computer's power cord takes a machine that requires a certain current and turns it into a machine that takes a different current. Middleware are used to do all sorts of things.

So, for instance, there's a Middleware called Prone that captures exceptions in the Handler and displays them in a nice format. Prone is a function that takes a Handler and returns a new Handler that catches exceptions and returns a different Ring Response in that case. Or you have Middleware that handle session cookies. The Middleware take a Handler and return a new Handler that understands sessions.

My recommendation for what to put in Middleware versus what to put in Handlers is simplest to explain with a graph.

Along the x-axis, we have logic that ranges from HTTP logic (handling header values, query params, etc.) to business logic (which bank account to withdraw from). Along the y-axis, we have how unique the logic is, ranging from highly duplicated to custom. These are the two axes I use to figure out whether it should be in the Handler or the Middleware.

The clear cases are easy. In the upper right corner (red dot), where it's custom business logic, it's definitely in the Handler. In the lower left (blue dot), where it's duplicated HTTP logic, I prefer Middleware. The hard part is in the middle. Somewhere between those two, there's a fine line where a case-by-case decision is required.

Conclusions

Ring is great because it requires so few concepts to capture so much of HTTP. But it's not everything. Standard Ring does not support WebSockets, for instance. Small adaptations are necessary. In general, I think this is a great abstraction. And Ring is so central to Clojure on the Web, it's important to know.

If you want to learn more about Ring and how to construct web applications in Clojure, you should check out the Clojure Web Tutorial. It's free and takes you through the process step-by-step.