NestJS Metadata Deep Dive.

Jay McDoniel | Trilon Consulting
Jay McDoniel

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]reflect API. In doing so, they are a useful tool for what is called [meta-programming]meta, 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
#NodeJS
#Documentation

Share this Post!

📬 Trilon Newsletter

Stay up to date with all the latest Articles & News!

More from the Trilon Blog .

Kamil Mysliwiec | Trilon Consulting
Kamil Mysliwiec

NestJS v10 is now available

Today I am excited to announce the official release of Nest 10: A progressive Node.js framework for building efficient and enterprise-grade, server-side applications.

Read More
Manuel Carballido | Trilon Consulting
Manuel Carballido

Implementing data source agnostic services with NestJS

Learn how to implement data source logic in an agnostic way in yours NestJS applications.

Read More
Jay McDoniel | Trilon Consulting
Jay McDoniel

NestJS Websockets Messages and RxJS Subjects

Learn how to send dynamic events from the server using RxJS Subjects and Websockets

Read More

What we do at Trilon .

At Trilon, our goal is to help elevate teams - giving them the push they need to truly succeed in today's ever-changing tech world.

Trilon - Consulting

Consulting .

Let us help take your Application to the next level - planning the next big steps, reviewing architecture, and brainstorming with the team to ensure you achieve your most ambitious goals!

Trilon - Development and Team Augmentation

Development .

Trilon can become part of your development process, making sure that you're building enterprise-grade, scalable applications with best-practices in mind, all while getting things done better and faster!

Trilon - Workshops on NestJS, Node, and other modern JavaScript topics

Workshops .

Have a Trilon team member come to YOU! Get your team up to speed with guided workshops on a huge variety of topics. Modern NodeJS (or NestJS) development, JavaScript frameworks, Reactive Programming, or anything in between! We've got you covered.

Trilon - Open-source contributors

Open-source .

We love open-source because we love giving back to the community! We help maintain & contribute to some of the largest open-source projects, and hope to always share our knowledge with the world!

Explore more

Write us a message .

Let's talk about how Trilon can help your next project get to the next level.

Rather send us an email? Write to:

hello@trilon.io
© 2019-2023 Trilon.