Centralized vs. Decentralized Routing in Web Apps
Routing is the act of describing possible URLs and the components of an app that should handle requests to these URLs.
Centralized routing #
In Ruby on Rails, which is probably the most influential web framework of the present,[citation] the different URLs accessible in an app are described in a single file, called routes.rb.
A simple example of a route setup could look like this:
SampleApp::Application.routes.draw do
match "/posts", to: "posts#index", via: "get"
match "/posts/:id", to: "posts#show", via: "get"
match "/posts/:id/like", to: "posts#like", via: "post"
resources :users
root to: "posts#index"
end
This example only uses a few of the possible ways to define routes:
match
takes a string that describes the exact URL to request. Theto
argument describes the"controller#action"
to be taken when the page is requested. And variable defined in the URL (prefixed with:
) will be available in the action.resources
automatically defines a bunch of endpoints for a RESTful resouce: indexing, retrieving, deleting, and updating is now automatically mapped from URLs to the corresponding controller-actions (eg.DELETE /users/1
would automatically point tousers#delete
).
Knowing a few conventions brings us a long way, and looking at routes.rb gives a good idea of what capability the app has.
This is what I call centralized routing: all the routes are defined in one place, and it works well as a kind of documentation.
A similar thing is possible using the Express web framework for Node.js. Here is a file that sets up the same routes for an Express app (Unfortunately without the nice shorthand of resources
):
function setUpRoutes(app, resources) {
app.get("/posts", resources.posts.index);
app.get("/posts/:id", resources.posts.show);
app.post("/posts/:id/like", resources.posts.like);
app.get("/users", resources.users.index);
app.get("/users/:id", resources.users.show);
app.delete("/users/:id", resources.users.delete);
app.put("/users/:id", resource.users.update);
app.post("/users", resource.users.new);
app.get("/", resources.posts.index);
}
But Express gives us another option, that is actually much more natural for the type of framework that Express is.
Decentralized Routing #
Ruby on Rails is an MVC framework. It is structured along the lines of models, views, and controllers, the different types of components that make up an app.
Express is a much looser framework. Existing basically as layer upon layer of middleware, it has embraced a functional approach. It is not structured along the lines of types, but enabled (and even encourages) structures aligned with the domain.
The domain is the real-world that your app represents. A post is a domain-concept. So is a user.
The app we described the routes for above can be defined as simply as this, using Express:
var express = require("express");
var userApp = require("./users/app.js");
var postApp = require("./posts/app.js");
var app = express();
app.use("/users", userApp);
app.use("/posts", postsApp);
module.exports = app;
What Express lets you do is basically say “I don’t know what happens in this endpoint, but that module over there does.” You let go of centralized control, and let each sub-app (parent-endpoint) take care of its own underlying structure.[1]
The underlying user app may look like this:
var express = require("express");
var resources = require("./resources.js");
var app = express();
app.get("/", resources.index);
app.get("/:id", resources.show);
app.delete("/:id", resources.delete);
app.put("/:id", resources.update);
app.post("/", resources.new);
module.exports = app;
The domain-oriented way of building web apps gives less of an inherent overview of the entire set of possible URLs. Instead, the structure of the app must be explored much like a file system: each new file reveals a new layer, a new set of capability.
On the other hand, in very complex apps, with many (hundreds) of endpoints, it is much easier to get an overview: “Ah, this system handles users and posts,” the reader of the code would think, “and I need to fix something with the posts, so I’ll look at that sub-app.”
It scales better because it naturally encourages low coupling. The domain-focused structure makes easy the act of Pushing Complexity, which is the act of making sure every layer of an application is understandable, and easy to get an overview of. This means writing small functions, maintaining small classes, keeping only small files, and limiting the scope of a module to the bare minimum, extracting everything else to separate modules.
With Express, we don’t need to keep all the web-facing code in one repository. It would be possible (easy, even) to turn the user app into a separate module, meaning that the user concept could be reused across different applications.
When writing a new application with the same model for users (but, of course, a distinct set of users), it would be trivial to depend on the same module.
The Web Framework’s Place in the World #
A web application should be the pipe between your business and the internet-connected public. It should not be your business logic, and it should not be inseperable from your business logic. If you have bad (lots of) coupling in your web application, meaning that it is not easy to separate your business logic from it, you have written a bad app, and you have become framework bound.
Most web frameworks (notably any MVC-framework) make you prone to becoming framework bound. They are split along concepts, meaning that each domain of your business has to be represented at several different places in the app: some in /views, some in /controllers, some in /models. Your business logic is spread thin.
If you write your business logic code well, it will be split along domains, be contained in small self-contained modules. Your web layer can then easily be added onto each of the outermost (interfacing) modules. Express allows you to handle each domain separately, enabling you to keep a good structure.
That is why, throughout this blog post, I have called it developing “in Ruby on Rails” (inside it), but “using Express” (it’s just a component).
To me it’s pretty obvious which one is preferable.
Notes #
- As it turns out, the same is actually possible in Ruby on Rails using Engines, but it comes a lot less naturally to Ruby on Rails than it comes to Express. ASP.NET MVC has the same capability. The main point of this blog post is what different frameworks encourage, and the point still stands: Express is pretty unique in encouraging decentralized routing.