How to Choose a NodeJS Framework
Or: How _I_ Chose a NodeJS Framework
How many times have you read a generic “Top 10 NodeJS Frameworks” article? How many times have you seen a listicle like that and breezed on by, knowing it was nothing more than click-bait for yet another “ExpressJS and some random alternatives” list?
This is not that article. I promise.
One thing I’ve learned while developing software over the past two decades is that there is no single “right answer” to a problem. We all know that, but the real lesson is that choosing between right answers can become the actual problem. You can trade one problem for another.
Frameworks are great examples of this challenge. Spring, Rails, Drupal, Laravel, Django — the list goes on and on. Every few years a new one emerges, and with it, a new religion with its followers ready to swear it’s the One True Way. Some of these frameworks are big enough that one can build an entire career around them, and I would never criticize those who do. There is value in growth along a consistent path, and inertia alone can justify all kinds of opinionated approaches.
Old joke: What do you call a good PHP programmer? Answer: Employed.
I’ve come to understand that reading a list of “top 10 frameworks” is doomed from the start. You can’t start with a list of options and try to pick the best. You must start with your own needs and wants and pursue the best fit. So let’s not talk about what’s best for you. Let’s talk about what’s best for me!
What Did I Want?
Throughout my career, my biggest lessons came from “what went wrong”. Every failure and RCA taught me more about how code should be deployed and scaled in production environments. Every launch delay proved to me the importance of minimally viable products and not writing everything myself, even if I could, and sometimes even if I could have done it better.
These lessons taught me to write as little code as possible to solve a problem, so I tend to prefer larger frameworks with more functionality, reducing what I have to write myself. Remember what Dijkstra said:
If debugging is the process of removing bugs, then programming must be the process of putting them in.
Every line of code you write is a potential future bug, misunderstanding, performance bottleneck, or tech debt. Write what you have to — but no more.
Equally important is the time savings. Any developer can knock out a simple Web service by pasting answers together from StackOverflow, but that doesn’t mean it’s “done”. Writing a production-ready application means documentation, automated unit testing, deployment tools and pipelines, QA/staging environments, and sorting out branching strategies and ticket workflows. If you spend 100% of your time just writing code, you’ll never have time for these other critical tasks.
My wish list for a framework is fairly long, but I boiled it down to five major areas:
I’m not a fan of code-as-config — I prefer config-as-code. Making function calls to define routes (as is done in ExpressJS) has always felt clumsy and heavy on boilerplate to me. I prefer a more streamlined routing table, preferably defined in a config file rather than as code modules. This goes for other config/setting-like elements, as well.
Declarative Action Handlers
Wikipedia has a great definition of declarative programming, but it’s a little vague about how it applies to a real-world service stack.
To me, it means request handlers include metadata that define and describe them, their parameters, and their middleware. I find services built this way to be much easier to maintain over time, especially by larger teams. Every mystery avoided makes on-boarding a new developer that much easier.
As soon as you scale an app horizontally, all kinds of orchestration requirements pop up: cross-cluster RPC, cluster-aware “chat room” mechanics, shared cache layers, etc. PubNub is great, but frequently becomes a significant portion of the ops cost for my projects. I wanted something I could build on internally, as part of my core architecture… without having to write it myself.
WebSockets as a “first class citizen”
Too often, WebSocket support is bolted onto a stack late in the game, usually when somebody wants to throw in live chat or discovers their mobile app is burning too much battery power. I wanted a framework that supported WebSockets at every level, and made it irrelevant whether a request came in via HTTP or WS. “It should just work.”
This one is a no-brainer. Support for defining middleware makes it easy to insert pre- and post-operation code to support global tasks such as session authentication, audit logging to DataDog or NewRelic, etc. Frameworks that have robust support for middleware also tend to have collections of third-party plugins you can leverage to shortcut your dev cycle.
Batch task processing
Most app stacks expect you to ship background tasks out to a separate service like Celery. That’s fine, but I’ve found that these tasks frequently need access to the same databases, support code, Redis caches, etc. that the API layer uses, because they’re continuing or completing work started there.
This means you must choose between duplicating the code used to access these components, or extracting that code into a shared module. Sometimes you want to do that anyway, but it’s a heck of a forcing-function if you only needed to send an e-mail. Any framework that has some kind of task-management component included gets extra marks in my book for this convenience.
What’s Not to Like?
Early in the build cycle for https://firetalk.com/ I spent a month going through literally EVERY Node framework available, looking for the perfect fit. IIRC there were more than 2 dozen options, and I won’t waste your time re-listing them here. After all, I did promise that this wasn’t a listicle… Instead, let me just summarize some of the key findings, then you can decide how they influence your own choices.
Every Node framework evaluation should start with ExpressJS or Koa. These are simple, elegant options with very wide community support and plenty of battle-hardening in production environments. But as soon as you use either one you realize they’re also very low-level frameworks— they do just a few things very well, and nothing more. I actually still use Express in several micro-service projects, but as soon as a project starts growing, I reach for a bigger tool.
Any Ops or Swagger fanatic should also look at Strongloop. This stack gives you a super quick way to build an API visually and then deploy it with good framework and ops/monitoring tool support. If your needs start and end with just creating an API, you may be able to stop here and be happy. I couldn’t do that. My projects have always needed the additional chat, cross-cluster RPC, and other items I listed above, and Strongloop always fell short at that point. This is the one I wanted to love, but it never quite fit properly. Kind of like AppEngine.
Love Rails? Try Sails! (I don’t love Rails. I didn’t like Sails. Moving on.)
Hapi nearly made the cut. It doesn’t provide that much more functionality than Express/Koa in its core, but it has a very large collection of plugins and some of them can be combined to create a pretty powerful stack. I ultimately decided against Hapi not on merit but because at this point in the evaluation I had found ActionHero (see below). Had ActionHero not been an option, I might be using Hapi today.
I’d be remiss if I didn’t comment on Meteor, which seemed great at first glance — it’s fun to toss together a quick live-synced todo list app with minimal server code. But at least in my experience, when your business logic becomes significant, frameworks that try to be too smart about moving data around usually end up just getting in your way. This POC ended quickly. I suppose it didn’t help that our team at the time strongly preferred SQL databases. Meteor fits best with Mongo.
Those were the biggest entries, but the list was huge. I have a screen shot of my browser tabs saved from the time we did the evaluation — the titles aren’t even recognizable. This is just a portion of it:
But some days you just get lucky. Halfway through our POC, we had one of those days — a perfect fit.
ActionHero (AH) is an interesting framework — it’s much more feature-rich and opinionated than the low-level options like Express, but, unlike Sails, it does not dictate specific code patterns within your handlers (e.g. MVC). Instead, ActionHero projects are structured around six core features that make it easy for a new developer to get started in a project:
- Config Blocks — Standardize how configuration data is stored, and how QA/Production environments override default settings. Routing tables are handled here in a very readable and easy-to-maintain section. AH also makes it super easy to deal with config differences in Dev/QA/Production.
- Actions — Although it’s not enforced, the standard is one-file-per-handler for API calls. and they’re automatically discovered and loaded. Each contains both the handler callback and a metadata block that defines, describes, and configures input requirements, validators, documentation blocks, middleware settings, and more. Goodbye, mysteries!
- Initializers — Middleware and library components. This is where you load Sequelize, connect to DataDog, add analytic trackers to API endpoints, and so on, then make those services available to your action handlers. Hooks are provided to allow middleware to intercept requests on their way in, or post-process them on the way out.
- Tasks — You can define both recurring and non-recurring tasks here and just like actions, each has both a handler and a metadata block that configures the task. The task layer is built on top of node-resque, so it’s easy to set up dedicated batch-processing nodes, priority queues, etc.
- Support for Web, WebSocket, and raw TCP client connections. Connections are abstracted so actions don’t need to know how a request arrived (unless they want to). This makes it easy for mobile clients to do things like primarily use WebSockets, but “fall back to” HTTP calls when networks get shaky.
- Batteries-included components — ActionHero ships with cluster-aware chat room mechanics, caching, logging, and file-serving facilities that give you a big running start when building a new application. Although these are bundled with the core module, they were built using the same features listed above, typically as middleware components. That makes it easy to pick these modules apart to see how they work, or even enhance/override their behavior when necessary.
I’m now well into my third significant ActionHero project, and it’s still a great fit for my needs. My experiences may not mirror everybody’s, but I do hope sharing them has given you some food for thought in your own search!