Node API with NestJS, is it worth it?

2023-06-12

I wanted to try out NestJS for a long time, but never had the chance to. So instead of sitting on my hands I decided to refactor my Fastify REST API to use NestJS.

I’ll be honest (and maybe biased by not using backend frameworks in Node for a long time) but I hated every moment of it. What I thought was a great framework turned out to be fragile, somewhat dull and with very questionable standards. I think some of the design choices are very poor.

I won’t focus much on the features provided as the idea was to offer all the features (while possibly being 100% compatible) with the previous API. You can look at the code yourself here.

The Good

Let’s start with the good news first!

Code Organization

Nest took inspiration from Angular modules, and the result is great. You can put all af your code for one feature in a module and call it a day. The files are close together and neatly organized.

🌳 nest-api
 └─ 📁 src
    ├── 📁 feature-1
    ├── 📁 feature-2
    │   ├── 📄 feature-2.controller.ts
    │   ├── 📄 feature-2.module.ts
    │   └── 📄 feature-2.service.ts
    └── 📄 main.ts

You can also put shared functionality in its own module and import it in other modules.

🌳 nest-api
 └─ 📁 src
    ├── 📁 db
    │   ├── 📄 db.module.ts
    │   └── 📄 db.service.ts
    ├── 📁 todos
    │   ├── 📄 todo.controller.ts
    │   ├── 📄 todo.module.ts
    │   └── 📄 todo.service.ts
    ├── 📁 users
    │   ├── 📄 user.controller.ts
    │   ├── 📄 user.module.ts
    │   └── 📄 user.service.ts
    └── 📄 main.ts

When building anything in ‘unstructured’ languages like Node or Python, you get to see all different kinds of folder structures. I think that having a solid convention like Angular’s is a good thing, it’s really easy to lose yourself otherwise. Especially when working on multiple projects.

code organisation meme

Nest CLI

Nest’s CLI, much like Angular’s allows you to quickly bootstrap your applications. It just works and saves you quite some time. I personally don’t like that much the idea of needing a CLI to generate files, but I understand that need just by thinking about some giant monoliths I worked on in the past.

Dependency injection

A dependency injection system is one of the features I appreciate the most of .NET, it really reduces the amount of complexity you need to handle in your project. Nest took inspiration from Angular’s DI, so that’s no stranger. I previously fiddled with tsyringe and will say that I feel right at home.

The Bad

And that was just about it for the good part. On to the downsides then.

API reference

Nest’s wiki is quite extensive and provides examples for the most common use cases while showcasing the framework’s features. What I couldn’t find is an API reference that actually lists all the functionality. On multiple occasions I had to go through both the code and stackoverflow. There’s a PR about adding an API reference that is still open after three years.

Structured logging

NestJS by default does not output structured logs, instead it writes them in a human friendly format. I like to be able to read logs when I’m debugging, but in a production context I need structured data to be able to filter them effectively and efficiently. The framework doesn’t provide it, so I used nestjs-pino to output structured logs with the fastest Node.js logger. The fact that both pino and this plugin is not mentioned once on the wiki really meakes me raise an eyebrow.

Docker support

Almost anything can be dockerized, and Nest apps are no exception. But providing at least a sample Dockerfile would have been nice. There are no articles on the docs and no examples in the github repo. At least acnowledge that Docker exists, it’s 2023!

The Ugly

This is where I think both limitations and bad design choices of the framework are made clear.

These are the things that made me really hate the experience.

Decorators

NestJS uses decorators heavily. That can be a very good thing, it makes the framework look composable and clean right?

Does this look clean to you?

@Put(':id')
@ZodSerializerDto(ResponseDto)
@ApiParam({ name: 'id', schema: paramsSchema.properties!.id as SchemaObject })
@ApiOkResponse({
  type: ResponseDto,
})
async update(
  @Param() { id }: ParamsDto,
  @Body() todo: BodyDto,
): Promise<Todo> {
  return await this.service.update({ ...todo, id });
}

Readability isn’t the only thing that decorators affect.

I got really weird errors in the dependency injection system and a really strange bug that replaced some openapi schemas with others. These are not the kind of errors that make you grow as a problem solver and they only make you angry.

Decorators in javascript are quite a controversial topic and I really question the decision to use them. But I can’t complain too much since that’s what Angular uses, and NestJS again took heavy inspiration from it.

decorators meme

Still, I don’t really like clever code. The way decorators are used in Nest looks like a clever way to write less code… that lead to unpleasant debugging sessions.

Validation / Serialization

Now that I tried libraries like Zod and Typebox I’m never going to use things class-validator and class-transformer again.

So I didn’t use them, and went for nestjs-typebox instead, so that I could reuse my schemas from the previous project. I couldn’t manage to make swagger work with it, so i abandoned it in favor of nestjs-zod.

Well that absolute garbage code snippet you just saw earlier is the result. You need to be very explicit and provide the openapi schemas through decorators and that really sucks. At this point I thought about going back to class-transformer, but after reading this horrible warning I decided not to.

I still can’t understand how people manage to use these libraries and not think that there must be a better way to do things. I also don’t understand Nest’s decision to not consider Zod or Typebox to this day.

validation meme

Configuration Module

Well who could have thought that using the forFeature() method to register configuration for different modules wasn’t enough and that you need namespaces to provide the right configuration to your modules?

Not me, because the docs didn’t mention it once, and it took a long time for me to find the only issue about it (that only gave me a hint of the solution).

Tests

I expected the testing framework to be more, let’s say complete.

Nest does not provide an ergonomic auto mocking utility, so I used an external library to not write mocks myself.

Controller’s unit tests don’t test validation and serialization by default (and there’s no sane way to do it). Some stackoverflow answers on how to test them straight tell you to test them in e2e tests only. What am I going to unit test in a controller then if not presentation logic?!

e2e tests are just heinous, the way you have to write code from main.ts into your tests is a big L for me. Also, from the framework’s point of view it’s the way to go to test external services, like the db or a cache. There’s no concept of integration tests in Nest. I don’t like it that much, I prefer to have more control over what I’m testing. Here I’m testing many blocks all at once and the accuracy of the tests consequently drops. The coverage also gets worse.

Defaults in general

The defaults in a framework should be mature, robust but most importantly reasonable. I think it’s necessary to keep them updated and change them in major versions when something else fits in better. It looks to me that this is not the case for NestJS, I mean for the defaults we have libraries like:

All libraries used by Nest can be considered mature and robust, but I don’t think they are all reasonable choices, some of them at this point have cobwebs!

libraries meme

Final thoughts

Let’s just say that with all opinionated frameworks the moment you stray out of the path that has been laid before you things get increasingly complex and clunky. It happens in Spring Boot, in ASP.NET and it happens in Nest.

What I didn’t expect was the lack of support for the functionality I had to provide to be fully compatible with my previous project (something that is very likely to happen when refactoring a real service). Just by having these features I ended up going miles and miles in the ‘wrong’ direction. This never happend to me in other frameworks, usually because they offer saner defaults.

In the end, in the context I played around in, which is the usual beautiful monolith rest api, I think NestJS provides no value to me and just slows me down. It also forced me to write lower quality code and decreased the testing surface. To me NestJS looks more like yet another problem rather than a solution to an existing problem, which is standardising backend development in Node.

Don’t get me wrong, I really think there’s a great effort coming from Nest’s contributors in this ultimate goal, but the result really falls short. The concept of a complete framework is wonderful, you can see the amount of problems they covered just by glancing at the different section’s titles in the docs. But ultimately, I find the realisation of this concept is very poor.

I look forward to experiment with it in the context of microservices, where it might be able to win my approval back with its built in messagging patterns and solid code organization.

But for the time being, and for the use case I explored, I don’t recommend using it. At all.