So you’ve decided to learn Rails.
You could dive straight in to learning the code, but Rails is an opinionated framework and it expects certain things to be done a certain way.
A high-level understanding of how it’s structured and what goes where is always important. So let’s follow a hypothetical web-request on its journey through a Rails app.
The world outside your app
Someone fires up their browser and types www.mysite.com into the address bar. DNS does its thing, resolves to your IP address and parcels up an HTTP GET request and flings it over the ether.
What happens to it when it hits your data-centre depends upon your exact setup (in particular, the web-server, cache and load-balancing layers are likely to be different each time), but for most of us, it goes something like this:
The first thing is the request gets routed through the data-centre till it hits a front-end web-server (Nginx would be a good example). The web-server looks at the request and forwards it on to a cache – often a piece of software called Varnish. Varnish examines your request, decides if it’s been asked for something similar recently, and if so returns the same response as last time. So, in the right circumstances (which you can control using HTTP headers) requests never even have to hit your Rails app to produce a meaningful response.
If Varnish can’t deal with your request, then it passes it on to a load-balancer (sometimes implemented in hardware, sometimes using software such as HAProxy). The load-balancer looks at where your request’s destination and decides which app-server is best placed to deal with it. To do this, it monitors the various app-servers and decides which one is least busy and most able to respond quickly.
The app-server is where the fun really starts.
Entering the world of Ruby
I tend to use Puma as my Ruby app-server nowadays, but alternatives would be Passenger, Unicorn, Thin or even just Webrick, which ships with Rails. All of these use a simple Ruby interface called Rack to deal with the incoming HTPT request. Rack unpacks the request and forwards it on to your application itself.
At this point, you may expect your Ruby code to be invoked. Well, not so fast.
Firstly, it hits a layer called Rack Middleware. Middlewares are like filters that apply before and after the web request is dealt with. Big chunks of Rails are implemented as middlewares, and you can insert new ones into the chain if you need them – it’s useful for caching, or implementing something that’s conceptually outside your application like authentication (using Devise or Omniauth).
But if the request isn’t completely dealt with by middleware, it finally reaches some of your Rails code.
The first place you have some influence over this request is in the router. Here you map incoming URIs to actual pieces of your Rails code – if it’s a GET request for /documents/123 your config/routes.rb tells Rails to send the request to DocumentsController and invoke the show action (in fact, it creates a new instance of a DocumentsController, triggers the relevant action, passing in the request object, then once the controller has done its thing, the controller is destroyed).
There’s a lot of stuff you can do with routes, but for the most part, you want to stick with Rails’ Restful routing convention – it’s quite constraining but it makes your application understandable to all.
The controllers are the first bit of your application proper. The controller’s role is vital, but often hard to explain.
Basically it mediates between the outside world and the internal space of your application. You’ve had a web-request, with various parameters, IDs, integers and strings, HTTP authorisation headers and all sort of esoteric stuff that is nothing to do with the domain of your application. The controller’s job is to turn that stuff into People, Users, Invoices, FriendRequests, Likes, tell those things to do stuff and then decide what to show back to the user.
The most common way for a controller to do this is to load one or two models, send messages to them and then send those models to the views.
(I actually disagree with this, but I’ll deal with that another time).
Here’s our DocumentsController’s show action, as described earlier:
@document = Document.find params[:id]
We’re translating an incoming integer (stored in params[:id]) and using it to find something meaningful to our application – in this case Document Number 123. We then store that in an instance variable and Rails magically invokes the /app/views/documents/show.html.erb template, giving it @document.
In a traditional Rails application, the models are the nuts and bolts of your application. As mentioned above, they are People, Users, Invoices, FriendRequests, Likes – whatever your application is about. Most of them are ActiveRecord models, meaning that they are stored in a relational database, but there’s no need for that to be the case; they could read information from another web-service, they could just be a stand-alone object that performs a useful calculation.
But most of the time, they’re ActiveRecord objects. Which means we load them, we update them, we store them.
Each time we perform ActiveRecord operations, Rails makes a request to our database. To do this, ActiveRecord reads its configuration from config/database.yml and maintains a connection pool (so that it can reuse existing database connections for efficiency). It also caches queries – if you read Document 123 from the database repeatedly in one web request, it should only hit the database once. Eventually, the data is returned to ActiveRecord which then squashes it into a model for you.
So our controller has loaded up some models, told them to update themselves and stored them in instance variables. We then either redirect or render a response (Rails can also do streaming responses, but that’s a whole different story).
A redirect is a specific HTTP response – a status 302 which tells the user’s browser to move to a different URL.
A render is a bit more complex – Rails takes those instance variables from your controller and shovels them into another object, a View. This is a bit weird from a programming point of view – you’re taking private state from one object and putting it into another – which is a reason I try to avoid doing it.
But your view then takes the contents of those instance variables and outputs a response from them – normally HTML, but it could be JSON, XML or anything else.
Rails takes the output from your view, packages it up as a HTTP response and sends it back through the middleware (which may mangle it further), app-server, load-balancer, cache and web-server till it eventually reaches your user’s browser and is shown on screen. By setting various HTTP headers you can also tell those outer layers to cache the output in various ways if required.
So there you have it – the life-cycle of a Rails request. It’s all built as a series of layers, each one has its own distinct role. In particular, in your application itself, packaging and mediation belongs in the controller, your application-specific logic belongs in the models (or at least in the model layer, if not the actual model objects, as I’ll explain another day) and your output belongs in the view.
Try to avoid mixing them – don’t have authorisation in your models, don’t make decisions in your views, and your Rails app will be much simpler and easier to follow. And you will be much happier.