> For a long time NestJS depended on class-validator ... and regularly suffers from security vulnerabilities that can take years to fix.
As a maintainer of class-validator, I'd like to clarify that this is not accurate. Legitimate security issues, when reported, are promptly addressed. The multi-year security alert listed in NIST NVD is akin to the bogus report that the curl maintainer discussed a few months ago.
In a nutshell, the report suggests that specific settings can potentially lead to validation bypass, which is indeed the case because these settings determine whether unknown objects should fail or pass the validation. This is analogous to my creating a CVE for Windows simply because anyone can access my computer when I haven't set a password.
However, the other part about the scare support is sadly true though.
I'll say it: NestJS is pure evil. It pushes OOP beyond its absolute limits. Not that I would consider OOP a good way to do things in general. Beyond misleading autocompletions that look helpful at first glance, it's usually somewhat counterproductive in any language and with any framework I have ever encountered. But NestJS manages to make it pure pain and sadness. It's like a framework that spins around its own "architecture" making it nearly impossible to get stuff actually done.
Pro tip: If you find a paragraph celebrating "SOLID principles" at the beginning of the documentation of something, run.
> Pro tip: If you find a paragraph celebrating "SOLID principles" at the beginning of the documentation of something, run.
It's been a while now, but can't recall how many times I've been asked about SOLID and in reality the codebase was as solid as wet paper. It felt more like a buzzword thrown around so people can argue about hypothetical, perfect OOP code. So glad FP is more widespread
"Not that I would consider OOP a good way to do things in general" this statement doesn't make sense to me. My normal mental model for design in usually "Objects" with message passing. How is OOP bad in general?
With circuit design you have OR gates and AND gates and NOT gates. However you can program everything with only NAND gates because with that gate you can express all other gates.
Ultimately though using universal NAND gates is a convenience of manufacturing and not comprehension. Just imagine if we replace all of our Boolean operators with an equivalent NAND and then only use that. Probably not the most readable code.
OOP has one trick: dynamic dispatch. Which corresponds to logical existential operator.
You could also have product (structures ... depending on if you consider getters and setters proper OO then maybe it has this), sum (sum types and discriminated unions), implication (anon functions), and universal (generics).
Yeah technically you can simulate everything with dynamic dispatch but then you kind of end up with a mess.
Smalltalk OOP is good, I'd argue nowadays OOP in modern lingo is not objects and message passing anymore (call it actor model if you want to be understood) but C++ / Java classes.
Namely I would say:
- Inheritance: bad because adding layers makes it harder to understand what code is running - favor composition instead
- Encapsulation: bad because it encourages mutable internal state and couple functions with data - favor immutable state
Plenty of practices of doing good OOP are all about doing less OOP (eg. Avoid deep inheritance hierarchies).
Aren't inheritance, encapsulation concepts of OOP more than guiding principles "to be followed"? Understanding inheritance vs composition becomes the basis for anything OOP, as are with other concepts.
Inheritance is considered harmful nowadays even if you ask the most hardcore OOP folks.
That leaves us with composition, which isn't unique to OOP and eliminates one of the core "benefits" claimed by OOP proponents. (Object oriented thinking basically. Animal <- Cat, Dog, etc.)
Your argument (as many others) falls into the category of OOP minimalism. Basically, it says: "Do less OOP". That's part of the philosophy for years now. It shows that OOP is essentially self defeating as there will be nothing of it left after every new "best practice" has advised us to get rid of each single OOP concept.
When the company I work for hired a person who was super excited about NestJS, I looked at it with an open mind (or so I'd like to think). I was probably a bit sceptical because this person, who seemed really nice, couldn't think of a single thing he disliked about it (that's a question I tend to ask about any tech that someone is all-over-the-moon about, and my thinking is that if you can't list a single thing you dislike about something, you probably don't know it well enough), but let's take a look at it!
As soon as I saw they had basically copy/pasted the Angular 2+ module system into Node.js, and went full-hog into IoC, I decided not to even try it out.
The same Angular 2+ module system that basically was copy/pasted from AngularJS and that I had actually ripped out of our huge AngularJS app at our previous job because it caused more friction than it solved, and lo and behold, the tests became simpler when we didn't have to use ngMock. All those services were singletons anyway and were more suited to being actual JS/TS modules.
But AngularJS had a valid reason for it because it was back in forever-ago and there were no JS modules, much less dynamic imports for code-splitting.
Not that you need code splitting for a backend app.
My big head blown moment with Angular was, when I discovered their IoC is singleton only. And the Angular community doesn't seem to be bothered about that fact.
I've been using NestJS this year and (mostly) found it great for the use cases I've had.
I'd rather describe the HTTP to method call mapping of my controller with decorators than a bunch of "add endpoint" calls. I like that the HTTP to code binding is located right beside the code itself.
I like that I can separate my app up into modules and couple them as loosely or tightly as I need.
You can architecture astronaut with NestJS, but you don't have to! I wrote a queue module (to interface with a legacy queue system), using decorators to specify the consumer. Docs on doing this are essentially nonexistent. The NestJS based library I cribbed from spread the "find the object & method to call" logic across multiple classes. I distilled it down to a screenful.
As with all tools, you get some choice in how you use it. Want to go wild with dependency injecting a zillion classes with one method each? If you really want to, I guess you can. Want to be sensible, for your definition of sensible? You can do that too.
Regarding TypeORM - recommending Prisma feels a bit like throwing out the baby with the bath water. If I'm reading their Github issues correctly, they are slowly starting to use JOIN statements in their generated SQL. It may be fine for an ORM to perform local joins during alpha, but anything after that really should use the features DBs provide for these kinds of basic things.
Finally someone said the words! NestJs always seemed like pure architecture astronautics, managing to be at the same time completely over engineered, and a copy of concepts from 2005 Java systems, in the worst way.
I have absolutely no idea why anyone would use NestJS, short of being used to writing dumb enterprise Java for the past 20 years or not wanting to move forward from it.
RE decorators in TypeScript: it is true that TS code does not have to live in classes... in general. But some APIs (eg those for custom elements) _do_ require classes, so one may end up writing a whole bunch of classes anyway. And in this case, decorators are in fact quite useful.
I don't use Nest or have strong feelings about it, but this bit stuck out:
> TypeORM is not even typed. When you run a .findOne() method is just passes back a generic.
This is patently false. I'm not sure what the author is even trying to say here, but the output of `Foo.findOne(...)` is `Promise<Foo | null>`. If it's not, there's a good chance you're coloring outside the lines. Maybe this is true in the context of NestJS, but it is not true for typeorm on its own.
I thought the same. It's not true with TypeORM in Nest either. If you're using DI it's basically impossible in Nest.
constructor(
// this is the only way to get a repository for the photo table/model
private photoRepository: Repository<Photo>,
) { }
getPhoto() {
// this would be type Photo
const photo = this.photoRepository.findOne();
}
I'm very curious to hear from someone who likes DI in a non-OOP language - why? What techniques do you use to make the level of indirection feel like a help, rather than a hindrance, in your codebase?
Reading this article reminded me that DI has a lot of advocates, but I've never heard a compelling reason to use it in most codebases.
How else are you going to manage dependent options of an algorithm but to inject the dependency inside of its configuration?
Say in rust, you have some struct backing a trait. But your implementation of the trait can also have a variety of options which are parameterized by some other trait. You end up with a generic struct: that is all dependency injection is. Every generic struct in rust is an example of dependency injection.
Instead, dependency injection frameworks help people instantiate large dependency graphs with some lifecycle management. Those I am a bit of the take it or leave it opinion.
I guess I don't typically think of things like Go's context when considering DI, but it serves conceptually the same purpose. My beef is more with the DI frameworks, I suppose.
I honestly think that DI frameworks don't add a lot of value (for me, when I work alone). You annotate some magic on a class or you call that constructor in your composition root. The explicit thing is nicer from my perspective, but most people don't know how to make it modular very well so the DI frameworks pop up to save people the trouble of learning. I prefer DI framework code to singleton spaghetti though, so...
You see that in OOP codebases where the authors don't know any compositional patterns. Then the only way to thread logic together is statically, so singletons and the enums like you mention.
Those that don’t know these patterns are way less of a problem than those who know them, know that it would be better to use them, but reject to do so. Because they are effectively lazy and don’t care about other people that have to work with their code in the future.
Unfortunately, there are many of those.
What I often find is that management in organizations rewards these people because they get quick results. The cost to the customer and the team is well hidden until it’s too late.
> I'm very curious to hear from someone who likes DI in a non-OOP language - why?
The alternative to DI is keeping dependencies in a global state (global variables, singletons, whatever) and that is a maintainability nightmare for any codebase above a certain size.
I don't think that it matters if the language is OOP or not, the issue is the same.
Dependency Injection allows for more incremental development in my experience. As each new class is simply added to a constructor somewhere and the system takes care of constructing things.
Regardless of what the article says, mocking in tests is easier. As you’re only depending on injected classes, you just mock a class and inject it in a test.
Like any architectural decision “it depends”. But I think it scales well, enforces modularity, automates construction logic (which can be huge) and is quite nice to work with generally.
> I'm very curious to hear from someone who likes DI in a non-OOP language - why?
The same reason as in OO languages. Clean separation of startup and runtime. Testability. Ability to avoid duplication and make changes in one place.
> What techniques do you use to make the level of indirection feel like a help, rather than a hindrance, in your codebase?
Separation of concerns/responsibilities always involves indirection. Using something like a free monad would be more indirection. I can't really give any useful advice beyond "factor your app well" and "split concerns/responsibilities at points that make sense"
I'm more curious to hear from Typescript developers who stand by using the global scope for dependency injection. I see that pattern a lot in Typescript code.
I suspect it is because Typescript doesn't properly support prototype function assignment, which makes scoped injection painfully ugly.
In my early 20s, I volunteered to be a figure drawing model. The class was run out of the domicile of an older fellow named Arthur, who lived off the coast of the Big Island in what would best be described as an art shack. It was more screens and windows than walls, but you can get away with that quite comfortably there.
The class itself a group of maybe 10-15 drop ins from the local commune, a mix of mostly straight women in their 20s and gay men in their 50s. It wasn’t really a formal class, more just the sort of thing that comes together when you’re surrounded by whimsical folk. It was fun being the center of so much attention and so many playful quips.
It was my first time, but the instructor Arthur said I was doing well and asked if I had any experience. Naked and near completely upside down, told him no, I was quite nubile. He asked if I meant a noobish. I doubled down and told him, no, I’m nubile, but excited to learn. He smiled at me, and gave me quite the flirtatious look. I had no idea until weeks later when I learned the distinction. Sadly, a few years later Arthur passed away quite suddenly. I’m always reminded of that experience every time I hear the word, and doubly so when it’s mistakenly used.
> NestJS is one of those things where people coming from other languages like Java don't know how to develop without the patterns that Java gave them.
This is EXACTLY how I felt using NestJS. Except, IMO, it took the worst part of Java (Spring DI, esp. the over-verbose, make-changes-in-4-random-files-to-get-an-object style they did it circa early 2010s, instead of the much cleaner styles they have today) and shoved it into a language that is built on a completely different paradigms and constraints.
Not had a single pleasant experience working with NestJS, especially once it hits a certain scale, and you are just bogged down running between modules, services, controllers and DI-hell
Dependency injection (i.e. inversion of control) is not standard OOP.
DI is not the only way to do composition in OOP. OOP gives you the choice of both inheritance and composition. Look at any Python, Rails, or Smalltalk codebase and you will see that things are both composed and inherited.
This post is just a rant. NestJS doesn't work for you? Don't fucking use it, no reason to hate.
Nestjs is my least favorite framework I've used in the past 10 years. Just a step below RxJS.
It's an ORM which means performance you may as well forget it. It's such a memory hog. On top of GraphQL, which can easily cause performance issues on its very own without help thank you very much. It's like the N + 1 problem had a baby with a memory leak. Why devs keep doing this to themselves I'll never understand. You don't need this level of pain for even a massive API codebase.
I've done a lot of work with NestJS. I agree with some, but not all, of the authors points.
- TypeORM is pretty bad. Particularly when you start using it for more than simple crud operations. There was an epic battle in the GitHub issues and eventually some folks finally got through to the maintainer. Since then, development has picked up and some long-standing issues have been corrected. It's still pretty bad. However, I haven't found anything in the nodejs ecosystem that's substantially better, which is sad.
- class-validator and class-transformer mostly work, but are barely maintained and have many issues going back years. The maintainers left a bad taste in my mouth. I asked years ago about why these libraries which are so core to NestJS are not forked. The issue was locked with a frustrating response. It sounds like they may have finally been pursuaded to fork them.
- NestJS support is terrible. I understand they have a paid support service but if you don't subscribe to that, they lock, discard, ignore any issues raised, even if they are valid and significant. They push all support questions to Discord, and as far as I can tell, none of the actual maintainers look at it. Very frustrating.
- NestJS breaks semver by releasing breaking changes in minor or patch updates. They don't seem to understand that if a core third party dependency has a breaking change, they need to bump their major version.
- It's very difficult to understand what has actually changed between versions. And with the history of breaking semver, upgrading is just plain scary. The "release notes" are just the commit list, and to make matters worse, they don't use a monorepo so you have to search through a dozen repositories.
Unlike the author, I do actually enjoy the modular design, the dependency injection system, and a few other design choices. The only difficulty I've encountered when testing is around the third party pieces, such as TypeORM.
The target audience also includes Angular developers. NestJS emulates several aspects, such as modules, that it doesn't necessarily need to mimic Angular's appearance.
I cannot pass judgment on whether this is good or bad, but I can easily see how this familiarity can be beneficial for the average programmer.
As a C# guy I liked NestJS as well. Like some of the things he describes as hating are the things I liked about it. I wrote a whole ass SAML server in NestJS, it was one of my favorite projects. NestJS is more than just GraphQL.
It is pointless for the author to argue about the best way to use TypeScript because the value proposition of NestJS is to use the same patterns as another framework you are already using.
If you're building a React app, for example, then NestJS doesn't make as much sense, it's just going to feel like it's getting in the way, hence this article.
This article really misses the mark and the author is just proving that they still don't understand what NestJS is for.
If you found this problems in nestjs then most of them should be in Angular as well. As it is explicitly told in the documentation that Nestjs is a copy of the frontend framework.
True, good code organization creates patterns and patterns create efficiency because humans are good at following them. When code is scattered and/or inconsistent in its design, I feel less productive. NestJS and similar modular frameworks do a good job at keeping code organized while allowing for scalability.
Also not a fan of NestJS, having used it on multiple projects in past job.
I'll tell you what. If you must work with NodeJS, IMHO you are best off just keeping things light and simple.
NestJS is not good. I've used it professionally a few years ago for several services, and it was overly complicated which would be fine if the performance was okay - but it was not good either. And don't get me started on TypeORM or Prisma (or ORMs in general). So much wasted time and effort hammering down performance issues. The NestJS docs were also not that great and required having to dive into GH issues or source code too much compared to other frameworks.
I eventually rewrote the services to use express + kysely + zod + msgpack. There was clear separation of concerns with layers which made both unit and integration testing easy. IMO attempting OOP in JS comes with additional complexity/overhead and performance implications due to overly complex inheritance.
I wonder what the alternatives are. I've searched for a framework that is a middle ground between bare-bones Express and NestJS. Something that I can throw a Zod validator at and it handles the validation for me by default, yet lets me structure my code a bit more functional and less opinionated than NestJS.
I don't know about Adonis, tried it the other day and followed their guide (maybe it was this one https://adonisjs.com/adonisjs-at-a-glance).
And the second or third command I typed in to create a model didn't work.
JavaEE has been using similar pattern for decades, somehow nodejs developers it's world changing and every startup on the planet is onboard with its ecosystem.
I like the part on tests. No need for dependency injection when you can just override/spyOn the functions. Most people in the .NET world uses DI, causing a lot of smaller classes where code is dubiously grouped. And my god, all the interfaces where every use of that could just as well be the class version. But we have to use DI, because (1) people look at you like you're insane if you tell them you don't like it. And (2), we also don't want to pay for commercial software like JustMock (not affiliated) or VS Enterprise (also not affiliated), that supports mocking static methods. I think that in lack of a free, maintained and widespread library for this, C# ends up with the mess the author describes. It's a shame, because there's nothing wrong with the language itself.
Because of (1) it's a struggle to convince anyone to buy (2). And would be silly to expect someone to provide it for free.
I remember back when I was working at a startup that used C# and then moved to a different startup where I was responsible for choosing framework, so long as it was NodeJS. Back then, the back-end TypeScript ecosystem was very small. A couple of the frameworks used IoC and DI, which of course were a standard feature of the C# back-end I had worked on. But I never really understood what IoC and DI were for, so I didn't use one of the bigger TypeScript frameworks that used them.
At the time, I thought I was dumb for not understanding the point of those things, but now, years on, I think those things really are just unnecessary.
I do like C# btw and would be happy to work with it again.
DI, at least in Java is a way to avoid rolling your own custom code to add dinamicity to the language. As you point out, JS, TS and Python are dynamic enough to not need those.
Recently I did some work on a project that was originally built using the LoopBack Typescript framework [0], and it suffers from all of the same issues as NestJS. I wouldn't recommend anyone use either framework. There's really no need for dependency injection in NodeJS. The problems DI solves in other languages can be better solved in NodeJS by other means.
Javascript adopting the class keyword was really bad. What a bizarre language which added something as syntactic sugar in the language but it's a paradigm.
I don't understand this perspective.. people were already using class semantics in JS before the class keyword; but there was so much inconsistency in the patterns they were using to (ab)use Javascript's prototypal inheritance to emulate classes.
The class keyword brings together a 'best practice' set of pre-existing JS features to provide a uniform OOP experience. Yes, classes still work a little bit differently than they do in other languages, but the benefit of being able to expect the same type of class from modern JS libraries doesn't get appreciated enough.
The only drawback I see to its addition is that it abstracts prototypal inheritance and some other key idiosyncrasies of Javascript from newcomers to the language. But the move away from doing things 'the old ways' has worked out much better for Typescript adoption anyway, as it's hard to type things properly when the interface of an object created by a custom 'class' function is just 'whatever the prototype currently has'
As a maintainer of class-validator, I'd like to clarify that this is not accurate. Legitimate security issues, when reported, are promptly addressed. The multi-year security alert listed in NIST NVD is akin to the bogus report that the curl maintainer discussed a few months ago.
In a nutshell, the report suggests that specific settings can potentially lead to validation bypass, which is indeed the case because these settings determine whether unknown objects should fail or pass the validation. This is analogous to my creating a CVE for Windows simply because anyone can access my computer when I haven't set a password.
However, the other part about the scare support is sadly true though.