In this blog

What is NestJS?

NestJS is an opinionated framework used to build "scalable server-side applications." It is heavily inspired by Angular and is mainly used to build APIs and micro-services, relying on dependency injection and decorator patterns for most of its usage.

For APIs, NestJS builds on top of ExpressJS (which can also be swapped out for Fastify), abstracting away some of the associated boilerplate for a much more opinionated approach to routing.

For micro-services, NestJS supports several existing options such as Redis, RabbitMQ and Kafka which are then exposed through abstract message and event decorators.

Our experience with NestJS

To save you the trouble of reading the whole article, here are my overall recommendations for using NestJS based on personal experience and the feedback of other teammates:

Do not use NestJS if:

  • You are building micro-services. The level of abstraction is too generic with overly simple examples in the documentation and it does not allow for the level of granular control needed. If you still want to use NestJS, I suggest creating custom services and not using the built-in micro-service abstraction.
  • You have complicated API arguments or responses that need good documentation and validation. While the existing decorators do allow for decent validation, complex types (especially with unions or inheritance) are nearly impossible to represent correctly without needing unintuitive workarounds.
  • Your project is a one-off. NestJS has a learning curve and even though it is built on top of ExpressJS, the two frameworks share very little syntax or ideas. If other related projects already use ExpressJS or Fastify, you are better off sticking with your existing framework rather than creating unnecessary mental overhead for maintenance.

Do use NestJS if:

  • You are an Angular shop. NestJS is heavily inspired by Angular and the syntax and concepts will transfer easily between the two.
  • You are prepared to make it your standard, want a quick way to have a TypeScript-first approach, and only need relatively simple API routes for your application.

API development

In NestJS, HTTP endpoints take the form of methods on classes, where the class has been decorated as a controller (which can dictate a URL prefix for all sub-routes) and the method has been decorated as a Get, Put, Post, etc. with a corresponding route. What this can allow for is applying decorators at the class level (like documentation or error handling) that are then applied to every sub route in the class, while still allowing individual methods to be decorated as necessary.

Unlike most Node HTTP frameworks, route handlers are not passed the request object by default and instead must declare their arguments through, you guessed it, decorators. This allows you to rely solely on Nest for extracting and presenting information in the correct way, such as query parameters, headers, converting the body of a post to an instance of a class, and more.

@Controller('/users')
class UserController {
  constructor(private userService: UserService) {}
  
  @Get('/:id')
  @Header('Cache-Control', 'max-age=60')
  getUser(@Param('id') id: string) {
    return userService.get(id);
  }
}

Pros:

  • Very easy to write simple API endpoints.
  • CLI tool to generate and integrate boilerplate code.

Cons:

  • Too much obfuscation. With ExpressJS (or Fastify) already fairly simple to use and understand, NestJS takes that too far, forcing you to actually learn more of what NestJS needs and less about what is actually going on under the hood. In fact, much of the documentation assumes you are already familiar with ExpressJS's concepts but still ends up using NestJS-specific ways of writing the code.
  • Too much magic. Similar to obfuscation, magic means things just work…until they don't. There have been several instances of having to fight or work against NestJS to meet a requirement that would normally be straightforward with something like ExpressJS. And again, the magic ends up meaning you know less about what's going on under the hood, leading to debugging that is less intuitive.
  • Lacking in features compared to frameworks in other languages such as Spring in Java or .NET in C#. This can be a hard sell in the JS realm as many follow the philosophy of single responsibility when designing a library or framework. That said, NestJS sells itself as pretty much self-contained which is not the case when compared to libraries available in other languages.

Documentation

Documentation can always be written separately from the application it is for, but NestJS allows for writing documentation alongside the source code in the form of decorators that output Swagger (Open API) docs. The argument here is that having both documentation and code in the same place means they get updated at the same time. However, the counterargument to this approach is that it can lead to having the opposite effect. Instead, developers may become nose-blind to documentation or, worse yet, trust bad/old documentation more since there is a stronger assumption of updates being made in parallel.

For validation and typing information, NestJS can sometimes use type signatures in order to generate documentation, such as if a property on a POST body is a number or boolean or string, but does not seem to be consistent and should not usually be relied on. Instead, the class-validator library can be used, which allows for more specific validation such as @IsEmail, @IsDate, @Length, @Max, or @Min as well as typing information. These properties can be used to validate requests (when a ValidationPipe is used).

The downside to the class-validator library is that it does not always get properly reflected in Swagger, forcing you to use NestJS's built-in @Api* decorators for documenting descriptions of requests (@ApiOperation), responses (@ApiResponse), query parameters (@ApiQuery), route parameters (@ApiParam), or request body properties (@ApiProperty). This can lead to having to duplicate or even confusing documentation that can become neglected easily.

@Controller('/users')
class UserController {
  constructor(private userService: UserService) {}
  
  @Get('/:id')
  @ApiOperation({
    summary: 'Retrieves information about a particular user.',
  })
  @Header('Cache-Control', 'max-age=60')
  @ApiParam({
    name: 'id',
    type: String,
    description: 'ID of the user to retrieve'),
  })
  getUser(@Param('id') id: string) {
    return userService.get(id);
  }
}

Pros:

  • Easy to start documenting with decorators.
  • Decorators mean your documentation and code are co-located.
  • Documentation and validation can be written with the same decorators.

Cons:

  • Complex documentation lacks examples.
  • Some features of Swagger just don't seem to work at all.
  • Documented objects cannot be merged or combined easily without losing said documentation.

Testing

At App Services, testing is always an integral part of any project. Fortunately, NestJS makes testing a breeze for the most part. Since it uses the dependency injection model, you can instantiate any of your classes in your tests with whatever arguments you want. NestJS also provides utilities like the testingModule which allows for better integration testing while still allowing you to mock at the edges of your application. NestJS also remains testing framework agnostic, so while Jest is recommended and is configured in the sample boilerplate, you could also use Jasmine, Mocha, or any other runner and assertion library.

describe('UserController', () => {
  const userService = { get: jest.fn() };
  let userController;

  beforeEach(() => {
    jest.resetAllMocks();
    
    userController = new UserController(userService);
  });
  
  it('should call the user service', () => {
    userController.getUser('test-id');
    
    expect(userService.get).toHaveBeenCalledWith('test-id');
  });
});

Since NestJS is typically used to write HTTP servers, testing your routes can also be very important. NestJS recommends using supertestfor full integration tests (tests that go from one edge of the application to the other). This allows you to choose if you want to mock at the database layer or use a test database for execution while avoiding the time cost of network overhead. Supertest can be a great way to test request validation and response for both happy and sad paths instead of needing to rely on truly end-to-end tests with a framework like Cypress.

describe('server', () => {
  let app, server;

  beforeEach(async () => {
    const moduleFixture = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    app.useGlobalPipes(globalValidationPipe);
    server = app.getHttpServer();
  });
  
  describe('/users', () => {
    describe('/:id', () => {
      await request(server).get('/test-id')
        .expect(200)
        .expect({
          id: 'test-id',
          name: 'Test User',
          email: 'test@example.com',
        });
    });
  });
});

For App Services, we ended up using Cypress for automated testing in our QA environment as it allowed us to test integrations between our API layers as well as our micro-services.

Pros:

  • Mocking is very easy due to the dependency injection pattern being used.
  • Batteries included for easy API testing with supertest. This allows for a form of end-to-end tests for the scope of the application (i.e. no external connections like databases or other applications).

Cons:

  • Need to remember to configure similar top-level module settings in tests compared to the app, otherwise, behavior can be different.

Conclusion

Overall, it is really hard to recommend NestJS at this time. Unless you are willing to go all in and commit to handling all its quirks and writing injectable service wrappers around third-party dependencies yourself, it is unlikely you will see any development speed or quality improvements over any of the other already-existing solutions out there. In particular, the documentation around libraries like Express and Fastify is far superior to that of NestJS and ends up giving you way more control. NestJS also does not have many uses at this time, so searching for articles or issues related to problems you encounter can prove fruitless more often than not.

This recommendation becomes even harder if you are not committed to using JavaScript in your backend. Other languages have much more robust and battle-tested frameworks to offer and should offer at least a comparable learning curve to that of NestJS.

It could be that as an ecosystem is built and the pain points are eliminated, NestJS may become a much more viable option for writing NodeJS apps. Many of the concepts employed such as the heavy use of decorators and dependency injection are good ideas and do feel like they have their place in JavaScript development, but the current implementation ends up feeling like too much of a black box for me to consider these upsides more valuable than the drawbacks.