I have been developing web services in NodeJS for fun and profit for  the past five years. Each time I started a new project, I have always  asked myself the following question:

Which server-side Javascript Framework should I use this time?

Javascript is full of options. And over the years, I have found  myself running through the same problems over and over again and solving  them each time using a slightly improved method over the last one. In  this post, I am sharing my experience with a set of the most popular  NodeJS frameworks.

Most of the web services I have written involve serving mobile  applications, without taking into account any frontend stack. Therefore,  in this writing, I provide input based solely on those requirements.

Imaginary scenario

Before I go deeper into the frameworks used, let’s take a moment to  determine some needs by defining a sample project. In the widespread  example of a web application for Managers Employees, and Departments  (slightly tweaked to fit a more elaborate use-case), a web application  should provide:

  • User authentication via username/password (for admins to manage their staff and for employees to change their info)
  • A role-based user authorization.
  • A  manager should only see the departments he/she is eligible for. As  such, an admin cannot manipulate employee data for a department for  which he/she does not have access to. (User authorization).  This means having the ability to check the arguments passed to an  endpoint against the user’s session data or an authorization token given  as header (JWT or anything else).
  • Employees should be able to change only their own information without affecting another employee’s assets.
  • Database connectivity — preferably using an ORM.
  • It  should have a mechanism to allow logging the request-ins and  response-outs of the API to external storage (for monitoring purposes) —  or the capability to add such a mechanism.

To cover the needs of this scenario (which includes a lot of everyday  use cases!), we need request hooks, database connectivity, an  authentication system, authorization hooks, and a fast way to implement  business logic without worrying for “glue code”.

Finding a suitable stack

I began NodeJS development without using any large frameworks — but  (like any other Javascript developer), I had to use a ton of libraries  to construct my stack (Hapi, Express, later Koa). Later in my career,  when I was searching for a suitable framework to avoid scaffolding my  service from the ground up, I chose based on the underlying technologies  used. I am in favor of building my development stack on my programming  style, rather than following the opposite path.

For all the web services I have constructed in NodeJS (except one), I  have opted to use a relational database (mostly Postgres), avoiding  MongoDB. The reasons behind those choices are beyond the scope of this  post. Let’s suffice to say that the implementations of those web  services demanded data integrity and database transactions, which  MongoDB (before version 4.0 ) did not support. Even if MongoDB database  transactions existed back then, there are some good arguments to be had  against using a Document store depending on your scenario.

I used Knex-based ORMs for making my queries, such as ObjectionJS or Bookshelf.  I love Knex. It has the best relational database connector, and it  makes query construction a breeze. It’s an absolute joy to use  Javascript for making relational queries using those libraries, which is  why I also suggest for you to treat Knex as a big bonus when deciding  your stack. If you prefer to use TypeScript, then you would want to use TypeORM or search for a web framework that uses it. You won’t regret it.

LoopBack

Loopback is  a beast, and one of the most well-known frameworks. Its most recent  version (v4) has been rewritten for Typescript, which comes in very  handy when writing application servers. StrongLoop (an IBM company, and  also the current maintainers of the Express library) are behind it.  LoopBack is polished and offers it’s own CLI for scaffolding.

Writing controllers is a matter of defining annotations on top of  methods, using a declarative approach, which departs from what Express,  Koa, and Hapi do (which is creating middlewares — instead of declaring  them using annotations).

// returns a list of our objects
  @get('/messages')
  async list(@param.query.number('limit') limit = 10): Promise<HelloMessage[]> {
    if (limit > 100) limit = 100; // your logic
    return this.repository.find({limit}); // a CRUD method from our repository
  }

It provides database abstractions through its database implementation  based on Juggler and supports the most common Database types, like  MySQL, Oracle, PostgreSQL, SQL Server, and more.

Those familiar with the declarative methods of constructing APIs will  understand how these have influenced Lopback’s design. Loopback offers  many abstractions that you need to learn before you dive into it.  Components, Datasources, Repositories, Models, Controllers,  Interceptors. The documentation does a very nice job explaining what  needs to be taught.

Loopback knows its strengths and plays on them quite nicely. It’s  oriented towards API middlewares and microservices and does an excellent  job of creating just that.

Loopback has developed its own stack for almost every feature it  supports, from database connectors to making HTTP requests. Therefore,  if Strongloop’s solution (or documentation about it) does not include  your specific use case, then you have to invest a good amount of effort  into figuring out its inner architecture and extend it. Where Loopback  really shines is when you want to create a middleware with first-class  Swagger support that connects your existing underlying infrastructure  with the outside world (possibly through a gateway). If that’s your use  case, then Loopback should be your first stop.

One problem I had with Loopback is how much opinionated, rigid and  “closed” it is (it’s open-source, but I am mostly referring to its  ecosystem and its extension mechanism) — especially when pitted against  its competitors. If you want to write custom logging interceptors, it’s  going to require a bit more work than the alternatives offered by the JS  world. It also does an excellent job of hiding its underlying  implementation. You learn the abstractions of Loopback, and you use them  for Loopback only. That’s not necessarily a bad thing, but it may  appear a bit “strange” to an experienced Javascript developer, as most  of the other frameworks give you a pretty accurate idea of what’s going  on under the hood. And since release 4.0 is relatively new, there are  not yet so many good references and solutions to common problems —  although, with this kind of backing, that’s bound to change fast.

If you are looking for an API middleware that allows static typing  running on the NodeJS platform, and you care about polishing and  corporate backing, then I propose to not make a pass on Loopback before  you try it.

Feathers.js

Feathers.js is  one of the most respected frameworks out there. It was created by David  Luecke a few years back and is based on top of the most well-known  open-source technologies to create a stack that allows the developer to  have excellent control over how an API behaves.

The architecture that Feathers follows is explained in a wonderful article written by its creator, “Design Patterns For Modern Web APIs”.  The post is not about Feathers, and this is why I strongly recommend  everyone to read it — Feathers builds on top of these patterns.

Feathers is using the Express middleware under the hood. Instead of  providing a database connector, it provides an abstraction layer called  “Service,” which is a RESTful representation of your defined model.  Services use a database adapter (which can be different for each  Service!) to retrieve the data and serve it using the REST API. In other  words, when you create a Model called “User,” Feathers automatically  creates a REST API for fetching, deleting, and filtering a User object.  After you declare your model and services, Feathers lets you aggregate  your services and provide your customized REST API on top of those  services. Since it provides Service abstraction layers, the HTTP  connector can be easily replaced with Sockets, thus making your API  real-time without any changes to your code.

The best part of all is that it also allows the developer for an  infinite amount of granularity. Each service has Hooks that enable you  to customize how the data of a model is going to be accessed and  filtered. Hooks is where you would put your authorization methods. You  can check if access is allowed on a model by putting this logic into a  hook that Feathers populates with the arguments passed for a Service.  Hooks are reusable (simple functions), so you can reuse the same hooks  across your application.

For our use example case (the Employee/Department system), Feathers  would be an excellent choice. It provides the scalability we need out of  the box, and it is geared towards universal web service patterns  without making assumptions about your use cases.

A few of its features:

  • CLI scaffolding tool.
  • Supports both Typescript and Javascript (Typescript support refined in the latest version)
  • Connectors  for the most popular databases (LocalStorage, MongoDB, SQL databases,  ElasticSearch), and more. Both Sequelize and Knex-based ORMs are  supported. Different models can belong to different database  connections. That means that you can create a service (that corresponds  to a DB model like User) that draws data from Postgres. Another one that  pulls data from MongoDB, without caring about how to aggregate them —  it’s seamless for the developer.
  • Express-like custom middlewares support.
  • First-class real-time API support.
  • It has excellent documentation.  Feathers is a “marriage” of open-source libraries, so it’s easy to find  solutions for any problem to the documentation of those libraries  themselves (for example, Objection.JS and Sequelize).

The amount of code you will write initially for  Feathers is more than both Nest.JS and Loopback. Still, once you have  your services in place, you will see that adding more features to your  existing codebase is going to take only a fraction of the time compared  to the alternatives. I have to say that Software Engineering — wise,  Feathers has the sexiest and most well-designed approach to developing a  web-service than any of its competitors. There is nothing you cannot do  with it.

Feathers has ended up being my all-round web service development  framework for NodeJS — although not the only one. I consider it the most  well-thought and developer-friendly framework to this day, and I can  only admire how far it has come without any backer like IBM behind it  (although it has a few).

Nest.js

Many consider NestJS as the Spring Boot for Typescript. And they are not wrong — but I would argue that its influences also include Angular.

NestJS is built on top of the logical foundations of Spring Boot (or  Java Enterprise?) and Angular. It treats Typescript as a first-class  citizen and fully supports it — in fact, it is written on top of it. Its  concepts are entirely new if you come from a “middleware” background  (like Express, Koa, or anything similar), but it’s certainly not new if  you come from the Java world or even the Angular (2.x+) world.

Nest’s basis is the concept of a Module. You register a Module, and  inside it, you register a bunch of Providers — which are component  instances that can be injected into other component instances. Their  purposes are to encapsulate functionality that can be provided to your  modules, like Database access, HTTP requests to external web services,  calculations, etc. You don’t instantiate the classes yourself, you rely  on Nest’s internal instantiation and lifecycle management, instead. Once  you have your Providers in place, you use them in Controllers — which  is where you expose your REST API endpoints.

How “Providers” are used. Image is taken from the official documentation at https://docs.nestjs.com/providers

To better illustrate this, we can use an example from the NestJS doc.  The following sample code shows how you would declare a Service that  would return cats from memory storage.

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];
create(cat: Cat) {
    this.cats.push(cat);
  }
findAll(): Cat[] {
    return this.cats;
  }
}

And this is how you would use it into a controller. Notice the  annotations applied to denote the method of the API and the parameters  it takes — it gives the impression that we are working with Spring Boot.

import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}
@Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }
@Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}

Nest supports filters, interceptors, middlewares, exception filters,  and guards. For database connections, it uses TypeORM — which is a very  well-thought Typescript-based ORM, with a large community. Nest  documentation is concise and straightforward — it leaves no essential  questions unanswered.

To sum up, I view Nest as one of the most polished and concise  frameworks to build your web service with. The Nest ecosystem brings  whatever you want to use in a large-scale app, such as Passport  Authentication, Database abstractions (using TypeORM), and a CLI that  facilitates scaffolding. I am using it for approximately a year now, and  I rarely find something missing. It is the most productive framework I  have used in a long time — and it is the one I would advise examining if  you go into microservice development.

Note: If you do choose to use NestJS, make sure you take the time to add true Hot Module Reloading to your application using Webpack. The default method that NestJS uses tends to get slower as your codebase is getting larger.

So what about Express.js, Koa.js, Restify, Hapi and the likes?

Except for Hapi, I consider those as libraries, not frameworks.  (NOTE: I consider Hapi to be on a league of its own since it offers many  functionalities you are going to need out-of-the-box).

With those libraries, you will most probably have to create your own  stack from the ground up or combine them with other libraries from their  surrounding ecosystem. You will have an infinite amount of flexibility  regarding your ORM choices and your authentication system.

I cannot say that this approach does not have its payoffs, but the  tradeoff is the massive amount of glue code you will write to create a  system/stack that suits your needs. And even if you do, you will  probably end up creating something that an open-source framework has  already done better (considering its contributors and man-hours put into  it). Let alone the fact that most other significant frameworks use one  of these libraries as their backbone.

Still, if you want to create a small to mid-sized API, using those  libraries directly is worth it. I lean slightly over Koa.js because its  approach with asynchronous middlewares allows me to do all sorts of  magic like logging, authentication, etc., without worrying about which  event should I listen to when throwing errors or appending data to an  asynchronous log. Then again, I have also used Hapi, and I was very  productive with it.

Conclusion

I had lots of fun using all these frameworks, while at the same time,  I was as productive as I could be. My choices when using them were  based on my programming style and my needs of the project at hand.

My goal in this post is to help you identify whether your use case  fits into one of those three and to provide something more than an  abstract sales pitch for each one.

That doesn’t mean that there aren’t any other frameworks out there.  Some of them may also be the right choice for your project, like AdonisJS, or Sails.js,  which (although I haven’t had the chance to use them), I highly  recommend trying, based on the feedback I have received by fellow NodeJS  developers.

As it happens with anything in life, you should always Try Before You Buy, and Test Before Implementing!