Working in Node.js consulting on the backend API team, I‘ve had the opportunity to work with various architectures, including Model-View-Controller (MVC) and Aspect-Oriented Programming (AOP).
Today, I want to compare these two popular design patterns. We will look at why MVC is the most popular and familiar. Then we’re going to have a look at FeathersJS, an AOP-based framework that was designed specifically for building clean, fast APIs.
What is FeathersJS?
FeathersJS is an API framework that was conceptualized and built by Bitovians. Bitovi has always had a focus on using clean patterns to “Do Things The Right Way,” and our enterprise experience shows up in the quality of FeathersJS. I like to think of it as the cleanest collection of best practices for building APIs. Oh, and you can trust me because I’m 100% biased. 😄 I’m one of the creators and long-time maintainers of FeathersJS.
What is MVC?
MVC is a well-known design pattern that separates the application into three distinct parts: the model, view, and controller. The model is supposed to represent the data and logic of the application, the view is generally the visible user interface, and the controller is intended to handle the communication between the model and view.
How a UI Pattern Took Over the Backend
Unless you started building APIs after GraphQL was created, If you have any backend development experience, you likely have seen or used MVC. It has been around for a long time, but MVC didn’t start out as a pattern for building APIs. It was developed back in the 1970s as a way of structuring user interfaces. It gained more popularity for UI work in the 1980s when Steve Jobs used the pattern to build the UI for NeXT Computer, a computer company he founded before Apple.
In the web-related world, MVC gained even more popularity in the late 1990s when it was used for Java Server Pages. The MVC pattern was still primarily used for writing user interfaces. In 2004 the world was introduced to Ruby on Rails, which used MVC to render, you guessed it, web-based user interfaces. A year later, some great PHP developers repeated the pattern, recreated Ruby on Rails in PHP, and called it CakePHP. Then in 2011, Laravel showed up and quickly became a favorite amongst PHP developers.
At the latter end of that timeline, a shift occurred. In the early 2010s, the Single Page Applications frontend architecture become popular. The frameworks began to take MVC (again, a UI pattern) and adapt it to build APIs.
MVC is Not a Great Choice for APIs
I’ve been programming long enough to have bitterly experienced some Java Server Pages work and each of the frameworks mentioned. I noticed that each framework was an iteration of the same pattern.
MVC has its positive points. For example, it clarifies concerns and allows for developing maintainable code that can scale when implemented correctly. But it comes with downsides you’d expect from adapting a UI framework for APIs.
For one, it can lead to code that's difficult to understand and maintain, especially when developers tend to prefer heavy controllers over heavy models. This anti-pattern can make it much more challenging to maintain an MVC codebase over the long run. When logic moves to the controller, MVC becomes less flexible, making it harder to modify the behavior of an application without cascading the effect into other parts of the codebase.
MVC also requires a significant amount of boilerplate code, which can slow down development time. Some of the most popular modern frameworks include generators that create as many as 17 files just to implement basic, queryable CRUD. Each of those 17 files represents more code to maintain. I often imagine Dr. Ian Malcolm from Jurassic Park and his famous sentence after first seeing an ancient MVC codebase that had grown out of control. "That is one big pile of MVC."
Despite these downsides, MVC is still the most widely used pattern, not because it’s the correct pattern, but usually because it's familiar. However, as Goldbloom's character warns, "Your scientists were so preoccupied with whether or not they could that nobody stopped to ask if they should." In other words, just because something is familiar doesn't mean it's the best solution for a particular project. When it comes to building a Node.js API, I prefer AOP-style programming.
AOP Makes Cleaner APIs
Now let’s take a look at Aspect Oriented Programming. Instead of breaking down code into strict layers of Model, View, and Controller, AOP allows developers to focus on building functions that handle cross-cutting concerns. The basic concept of AOP is that you have a chain of functions through which you pass a context object. Each function has the opportunity to modify the context or perform some side effects, like logging or sending a message, etc.
Most Node.js developers have used AOP in the form of Express middleware. In fact, middleware implementations I’ve seen and used in PHP and Node.js frameworks are AOP-like. Because these functions aren't restricted to a “model” or “controller” layer, they can be written in a way that makes them highly reusable across the CRUD methods of a single API service or even across multiple services.
Another nice aspect of AOP (that’s a pun) is that by focusing on cross-cutting concerns, you can find abstractions that apply across the entire API. For example, if you’re building a JSON-based API, you can build a utility that registers a JSON query parser and response formatter based on a request header. This is actually a pattern that’s popular enough to have made its way into some MVC frameworks. You can register middleware before going into the MVC layer.
One of the most significant benefits of AOP is that adding functionality to an API request/response cycle becomes very clear. You just register a function as a middleware in the order where you want it executed. But this is also where most Express-based APIs fall short. When you use a function to register each middleware, it’s easy for the middleware to become disorganized. Let’s take a look at how FeathersJS fixes this problem.
FeathersJS is Made of Clean AOP Middleware
The FeathersJS framework was originally built on top of Express, but its latest version has grown independent. It can still be used on Express, but the latest version runs on KoaJS or even in a Cloudflare Worker. Thanks to Feathers' excellent middleware layer, the experience is the same, no matter the underlying platform.
Feathers middleware, called Hooks, focuses on the central purpose of each API endpoint. That purpose is usually to read or write data in a database, but it could be any other single-purpose function. Feathers then wraps that single-purpose function in Hooks, which run before or after the function. You could think of it as an array of functions with the central function in the middle. It looks something like this:
There are several benefits to having a primarily-centralized place to handle middleware.
The order of execution is clear, and you can change the execution order of hooks by changing the order in the array.
Debugging is made simpler. No other middleware runs between these Hook functions.
Reusability of a hook function is clear based on the import.
Another great benefit to FeathersJS’s AOP approach is that it can handle multiple transport layers in the same Hook chain. For example, you can register a Socket.io transport and a Rest transport at the same time and handle requests for both at the same time! FeathersJS assures that all requests, no matter the source of transport, are piped through the same array of hook middleware. This means that you get real-time WebSocket support with zero additional effort.
Beyond AOP: More Healthy Patterns in FeathersJS
Apart from its excellent AOP implementation, there are a lot of interfaces in FeathersJS that save you time and lines of code:
Typed Client: FeathersJS’s CLI creates a Typed Client for you. With a single command, you can generate a client release and install it into your frontend without the need for a monorepo or a private, paid npm account. The TypeScript support is deep, too, giving insight into which services, methods, custom methods, data, and parameters are available.
Resource-Focused Endpoints: All API services are structured around — but not limited to — a RESTful interface. Where other frameworks generate 17 files for basic CRUD, Feathers can accomplish the same in about 20 lines of code and keep it fully customizable.
Custom Method Interface: If you can’t map your functionality into a REST method, you can create an RPC-style custom and don’t have to adopt a custom transport protocol.
Query Syntax Interface: Feathers pioneered using a common query syntax across any database. The Feathers community maintains a collection of dozens of database adapters that all can be queried with a common syntax. Official support for MongoDB and Knex-connected SQL databases is now built into the framework as of FeathersJS v5 Dove.
Resolvers Interface: In FeathersJS v5 Dove, we stole one of GraphQL’s best features and made it even more useful. You can use resolvers in any part of the request cycle. FeathersJS now supports Query Resolvers, Data Resolvers, Response Resolvers, and External Resolvers, which really simplify securing all communication, including real-time messaging. The resolvers are implemented as a special hook, so they fit right into the middleware chain, and you can register as many sets of resolvers as you need.
Feathers Schema Interface: is based on the universal JSON Schema standard and made prettier by TypeBox. By declaring your JSON schema, you get TypeScript types for free.
Feathers Errors Interface: Allows you to throw correct errors, customize them as needed, and even create custom error message types. All in a tiny footprint.
Feathers Configuration Interface: gives you typed configuration and runtime config validation to avoid weird production errors.
Feathers Authentication Plugins: allow you to implement token- and OAuth-based authentication with dozens of providers with few lines of code.
Feathers is Fast
Finally, FeathersJS v5 brought a huge request speed increase, with basic request-per-second benchmarks hitting over 50% higher than even Fastify. Let’s be clear, though: while microbenchmarks are valuable for gauging independent speed of one part of an API, by the time you add database interactivity to the benchmark, the total requests per second pretty much even out across frameworks. Developer experience and ease of use are far-better metrics for picking a framework. But there are circumstances where that speed is crucial, and Feathers delivers.
MVC is a well-known and widely used pattern. While familiarity often drives the choice of MVC, it is important to consider whether it is the best solution for a specific project. I consider it to be great for building UIs and ill-fitted for creating APIs. It has its downsides, especially when it comes to developing maintainable and flexible code for Node.js APIs.
For Node.js consulting, I prefer Aspect-Oriented Programming (AOP) because it allows for building cleaner APIs and focusing on cross-cutting concerns. Using AOP, developers can write highly reusable middleware functions that can be applied across multiple services. With AOP, adding functionality to an API becomes very clear.
I find that by using FeathersJS as the foundation for building APIs, I’m able to build faster with less code, which improves long-term maintainability. Even for developers who are accustomed to writing in MVC, taking a moment to adapt to “The Right Way to Do Things” will result in an increase in efficiency and money better spent.
What do you think?
We’d love to hear your thoughts about MVC vs AOP! Drop by our Community Discord to join the conversation. 🥚