█
LastWrite
  • > Curriculum
  • > Pricing
  • > For Educators
  • > About
  • > Contact
Log InGet Started

Questions, concerns, bug reports, or suggestions? We read every message, write to us at [email protected].

More ways to reach us →
LastWrite

Structured computer science lessons for aspiring developers and security professionals.

[email protected]

(201) 785-7951

Mon–Fri, 9 AM–5 PM EST

Learn

  • Curriculum
  • Pricing

Company

  • About
  • For Educators & Schools
  • Contact Us

Legal

  • Terms of Service
  • Privacy Policy
© 2026 LastWrite. All rights reserved.
Curriculum/Programming Languages/TypeScript/Decorators, Metadata, and NestJS
45 minAdvanced

Decorators, Metadata, and NestJS

After this lesson, you will be able to: Understand how TypeScript decorators and metadata work, where they power real frameworks, and read and write decorator-driven code in NestJS and TypeORM.

Decorators are functions that annotate and modify classes, methods, and properties. You may never write many yourself, but you will read them constantly: NestJS, TypeORM, and Angular are built entirely on them. This lesson demystifies the syntax, explains the metadata that makes dependency injection work, and shows the NestJS structure employers in the enterprise TypeScript world expect you to recognize.

Prerequisites:Declaration Files

What decorators are, and the standards situation

A decorator is a function prefixed with `@` that runs at class-definition time and can observe or modify what it decorates. Two flavors exist and the distinction matters in 2025: the legacy 'experimental' decorators (enabled by `experimentalDecorators` in tsconfig, used by NestJS, TypeORM, and Angular today) and the newer TC39 Stage 3 standard decorators that TypeScript 5+ supports natively. Most existing frameworks still use the legacy form plus `reflect-metadata`, so you need to recognize both. The mental model is simple: `@Injectable()` is just a function call that tags a class so a framework can find and wire it later.

Writing a simple decorator

A method decorator that logs calls. This is the shape every framework decorator builds on.

tsx
// A method decorator (legacy syntax, the one frameworks use today)
function LogCalls(_target: any, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`calling ${key} with`, args);
return original.apply(this, args);
};
return descriptor;
}
class PaymentService {
@LogCalls
charge(amount: number) {
return `charged ${amount}`;
}
}
// new PaymentService().charge(100) logs the call, then runs the method.
// Frameworks use this same hook to add validation, caching, auth, routing, etc.

ℹ️ Metadata and reflect-metadata: how DI knows what to inject

Dependency injection frameworks need to know a constructor's parameter types at runtime, but TypeScript types are erased after compilation. The bridge is `reflect-metadata` plus `emitDecoratorMetadata` in tsconfig: when enabled, the compiler emits type information that decorators can read via `Reflect.getMetadata`. That is how NestJS sees `constructor(private users: UsersService)` and knows to inject a `UsersService`. You rarely call the metadata API directly, but knowing it exists explains the otherwise-magical wiring.

NestJS: a decorator-driven backend framework

NestJS is the TypeScript-first Node framework (Angular-like structure). Decorators define everything.

python
import { Controller, Get, Post, Body, Param, Injectable } from "@nestjs/common";
@Injectable() // marks a class the DI container can provide
export class UsersService {
findOne(id: string) { /* ... */ }
create(dto: CreateUserDto) { /* ... */ }
}
@Controller("users") // routes under /users
export class UsersController {
// UsersService is injected automatically via constructor + metadata
constructor(private readonly users: UsersService) {}
@Get(":id")
getOne(@Param("id") id: string) {
return this.users.findOne(id);
}
@Post()
create(@Body() dto: CreateUserDto) {
return this.users.create(dto);
}
}
// TypeORM uses the same idea for the data layer: @Entity(), @Column(), @ManyToOne().

Common mistakes only experienced engineers catch

Forgetting `import "reflect-metadata"` once at the entry point, so DI silently fails to resolve types. Mixing legacy and standard decorators in one project; pick the mode your framework requires and set tsconfig accordingly. Writing heavy logic inside decorators instead of delegating to a service, making behavior hard to test and trace. Assuming decorator metadata exists without `emitDecoratorMetadata` enabled, then debugging 'cannot resolve dependency' errors. Reaching for NestJS on a tiny project where Express or Fastify (covered earlier) would be far less ceremony; NestJS shines on large, team-owned codebases.

Quick Check

In NestJS, how does the framework know to inject a UsersService into a controller's constructor?

Pick the correct mechanism.

Sign in and purchase access to unlock this lesson.

Sign in to purchase
←Declaration Files (.d.ts)
Back to TypeScript
Migrating JavaScript to TypeScript→