NestJS makes use of a powerful concept known as Dependency Injection. The idea behind this concept is that a framework, or some higher-level library, is able to create the dependencies for your classes and place them in the correct parameter position to create usable instances of your services, controllers, and other providers. Dependency Injection is a form of Inversion of Control, but delegates to the library or framework level, rather than having to maintain it yourself.
To give an example, let’s take a look at a simple nest new project and see how
the AppController
makes use of the AppService
via its constructor, even
though nowhere in the code do we as developers write:
new AppController(new AppService());
This is because Nest creates the AppService
instance during the
await NestFactory.create(AppModule);
call for us, and injects it into the AppController
’s constructor
Now, you
may be wondering “How does Nest know how to do this?” and the answer is simply
"metadata".
Of course, if everyone knew what that meant I wouldn’t be writing this article, so let’s dive into how this metadata even exists in the first place, so that Nest is able to make use of it.
Decorators
Typescript has supported decorators for quite some time now, starting with Angular back in Typescript 1.5. Now, to be very clear these decorators are not the same as the new TC39 Stage-3 decorators which are being supported with Typescript v5 and on. While similar in idea, the functionality is different, and for the sake of this article, when speaking about decorators, we are talking about the experimental decorators implementation, now known as legacy decorators.
Decorators, in short, are higher-level functions that usually just add metadata, key-value pairs that provide extra information about the target class, function, or parameter, to be read later on via the Reflect API. In doing so, they are a useful tool for what is called meta-programming, a technique where a program manipulates other programs as data. In this case, Nest treats the written server code as its own data so that it can create the express or fastify instance for you (or of course, the microservice, websocket, or graphql server).
Typescript’s own documentation on decorators goes more into depth on the types of decorators and how to write them. From here, we’re going to talk about what they do for Nest and why they’re important.
What Metadata?
When we use decorators with Nest, we are using them to create metadata entries
to later be read and used. This metadata is always read via the Reflect
API
and always takes the form of being read via Reflect.getMetadata(metadataKey, classInstance, methodName)
. The methodName
is only for when methods are
decorated, like controller methods with their HTTP verbs or GraphQL resolvers.
Arguably, the most important metadata that Nest ends up making use of is the
design:paramtypes
metadata. This is the metadata key that holds the
information about what dependency goes in what position of the constructors.
This is also used with the ValidationPipe
to know the type of each parameter
to the route handler, if that’s something that is used in your application.
It should be noted that the design:paramtypes
metadata only exists when the
--emitDecoratorMetadata
flag or emitDecoratorMetadata
config option is set
with tsc or similar compilation methods. This is the reason that some build
tools, like esbuild
, can end up struggling with building Nest applications
without extra addons.
The @Injectable Decorator
In the docs, it is mentioned that "The @Injectable()
decorator attaches
metadata, which declares that CatsService is a class that can be managed by
the Nest IoC container.". This is only a half truth. In reality, adding the
provider to a module's providers
array is what makes the provider able to be
injected via the IoC container. What @Injectable()
is doing is forcing
TypeScript to emit metadata about the class's constructor, specifically the
design:paramtypes
metadata that will be read at start up. If there is an
@Inject()
decorator in the constructor, by technicality this does enough to
make TypeScript emit all the same metadata. Take a look at the compiled
JavaScript from the following TypeScript class:
export class Foo { constructor(private readonly foo: string) {}}export class Bar { constructor(private readonly bar: string) {}}export class FooBar { constructor(private readonly foo: Foo, private readonly bar: Bar) {}}
The transpiled code would look like this
export class Foo { constructor(foo) { this.foo = foo; }}export class Bar { constructor(bar) { this.bar = bar; }}export class FooBar { constructor(foo, bar) { this.foo = foo; this.bar = bar; }}
Now, let's add the @Injectable()
decorator to the classes. We get the
following output in our dist
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, d; if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; };var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === 'object' && typeof Reflect.metadata === 'function') return Reflect.metadata(k, v); };let Foo = class Foo { constructor(foo) { this.foo = foo; }};Foo = __decorate( [Injectable(), __metadata('design:paramtypes', [String])], Foo);export { Foo };let Bar = class Bar { constructor(bar) { this.bar = bar; }};Bar = __decorate( [Injectable(), __metadata('design:paramtypes', [String])], Bar);export { Bar };let FooBar = class FooBar { constructor(foo, bar) { this.foo = foo; this.bar = bar; }};FooBar = __decorate( [Injectable(), __metadata('design:paramtypes', [Foo, Bar])], FooBar);export { FooBar };
There's a lot going on in the above snippet, so let’s start to break it down.
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, d; if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; };var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === 'object' && typeof Reflect.metadata === 'function') return Reflect.metadata(k, v); };
These methods look scary, absolutely, but generally we can think of them as setting up and adding values to the metadata object that can then be read later. We won’t spend any more time on how these methods work, but if you want you are more than welcome to spend some time with them.
Next, we have a simple class definition. Note, that I’ve specifically picked
out the __decorate
methods while walking through this. Don’t worry though,
we’re getting to it.
let Foo = class Foo { constructor(foo) { this.foo = foo; }};let Bar = class Bar { constructor(bar) { this.bar = bar; }};let FooBar = class FooBar { constructor(foo, bar) { this.foo = foo; this.bar = bar; }};
Nothing too special here, it is cool to see that Typescript adds the
this.foo = foo
for us, and it should be noted that the class Foo
is being
assigned to a variable Foo
which didn’t happen when we didn’t have the
decorators.
Now for the fun part that you’re here for, the __decorate
methods in actions.
Foo = __decorate( [Injectable(), __metadata('design:paramtypes', [String])], Foo);Bar = __decorate( [Injectable(), __metadata('design:paramtypes', [String])], Bar);FooBar = __decorate( [Injectable(), __metadata('design:paramtypes', [Foo, Bar])], FooBar);
Notice that now there are metadata definitions for the design:paramtypes
metadata key of [String]
for Foo
and Bar
and [Foo, Bar]
for FooBar
.
This tells Nest what parameters are expected in each position of the
constructor for each class.
When you use @Inject('SomeString')
Nest will set
design:paramtypes
to 'SomeString'
for the position that you are decorating.
What Nest will do when reading this metadata is match it up with the class or
injection token that exists in the current module's provider list and use that
provider in the proper location for the constructor. This is why it is very
important to either use a class, or @Inject()
on the right parameter.
Deep-dive further into NestJS internals
Check out the latest NestJS Advanced Concepts Course that dives even deeper into what we discussed above - which dives into more about Metadata, and other advanced Nest (and Node.js) scenarios that can come up in todays real-world projects!
Thanks for reading!
Be sure to check out more NestJS articles on the Official NestJS Blog, here at Trilon.io (the Official NestJS consulting/services team) 🐈
Learn NestJS - Official NestJS Courses 📚
Level-up your NestJS and Node.js ecosystem skills in these incremental workshop-style courses, from the NestJS Creator himself, and help support the NestJS framework! 🐈🚀 The NestJS Fundamentals Course is now LIVE and 25% off for a limited time!
🎉 NEW - NestJS Course Extensions now live!
- NestJS Advanced Concepts Course now LIVE!
- NestJS Authentication / Authorization Course now LIVE!
- NestJS GraphQL Course (code-first & schema-first approaches) are now LIVE!
- NestJS Authentication / Authorization Course now LIVE!