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.
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.
A method decorator that logs calls. This is the shape every framework decorator builds on.
// 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 {@LogCallscharge(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.
NestJS is the TypeScript-first Node framework (Angular-like structure). Decorators define everything.
import { Controller, Get, Post, Body, Param, Injectable } from "@nestjs/common";@Injectable() // marks a class the DI container can provideexport class UsersService {findOne(id: string) { /* ... */ }create(dto: CreateUserDto) { /* ... */ }}@Controller("users") // routes under /usersexport class UsersController {// UsersService is injected automatically via constructor + metadataconstructor(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().
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.
Pick the correct mechanism.
Sign in and purchase access to unlock this lesson.