Using three of the top NodeJS Web REST API Frameworks

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 repeatedly 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 considering any front-end 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 to which he/she does not have access. (User authorization).  This means checking the arguments passed to an endpoint against the user’s session data or an authorization token given as a 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 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 about “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 searched for a suitable framework to avoid scaffolding my service from the ground up, I chose based on the underlying technologies used. I favor 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 those webservices' implementations 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 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 to make relational queries using those libraries, which is why I also suggest you 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 its 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 creates 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 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.

I had with Loopback 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 will 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 not to 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 are 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, 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 will 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 middleware 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 will 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-around 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 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, 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 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 will 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 person-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 middleware allows me to do all sorts of magic like logging, authentication, etc., without worrying about which event I should 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 for 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 from fellow NodeJS developers.

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