Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

NestJS  Module 1 – Getting started with NestJS 12

Decorators, metadata and why Nest needs a build

How decorators and emitDecoratorMetadata tell Nest what a constructor needs, why Node.js cannot run Nest source directly, and three traps that erase it.

  • Beginner
  • 15 minutes
  • Examples run with Node.js 24.21.0, NestJS 12.1.2 and TypeScript 6.0.3
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Explain how experimentalDecorators and emitDecoratorMetadata let Nest find constructor dependencies
  • Show why Node.js's built-in type stripping cannot run Nest source files
  • Fix the import type, interface and missing-decorator mistakes that erase dependency metadata
  • Choose the TypeScript version that Nest 12's tooling supports

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain how experimentalDecorators and emitDecoratorMetadata let Nest find constructor dependencies
  • Show why Node.js's built-in type stripping cannot run Nest source files
  • Fix the import type, interface and missing-decorator mistakes that erase dependency metadata
  • Choose the TypeScript version that Nest 12's tooling supports

Before you start

On this page

Run a Nest source file with node and it stops before its first line of logic, even on Node.js 24, which runs TypeScript files by itself. Compile it without one compiler option and Nest starts happily, then throws Cannot read properties of undefined the first time a service uses what it was given.

Both failures come from the same place: Nest learns what each constructor needs from data that only a TypeScript compiler writes. This lesson shows that data in the compiled output, runs Nest source through Node.js’s own type stripping to see where it breaks, reproduces three mistakes that erase the data, and settles which TypeScript version a Nest 12 project should use.

A decorator is a function the compiler calls for you

The examples of this lesson use one small file, kitchen.ts: a menu service, a clock and an OrdersService whose constructor asks for both.

compiled/src/kitchen.ts TypeScript · compiled/src/kitchen.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class MenuService {
  today() {
    return ['poha', 'dal rice', 'thepla'];
  }
}

@Injectable()
export class Clock {
  now() {
    return '10:15';
  }
}

@Injectable()
export class OrdersService {
  constructor(
    private readonly menu: MenuService,
    private readonly clock: Clock,
  ) {}

  summary() {
    return `${this.clock.now()}: ${this.menu.today().join(', ')}`;
  }
}

@Injectable() is a decorator: A function applied with @ to a class, method, property or parameter, which can record data about it or change it. Nest uses TypeScript's experimental decorators, which a compiler turns into function calls.: a function that receives the class and may record something about it. JavaScript engines cannot run this syntax yet, so the TypeScript compiler, with experimentalDecorators on, rewrites it into ordinary calls. The session compiles the file and prints what it wrote for OrdersService, then lets Nest build the class:

What the compiler wrote, and what Nest built JavaScript · compiled/run.mjs
import { session } from '../session.mjs';

// Compiles kitchen.ts, shows what TypeScript wrote for OrdersService, then lets Nest build it.
session(`
npx tsc -p tsconfig.json
sed -n '/^let OrdersService/,$p' dist/kitchen.js
node dist/main.js
`);

Output

$ npx tsc -p tsconfig.json
$ sed -n '/^let OrdersService/,$p' dist/kitchen.js
let OrdersService = class OrdersService {
    menu;
    clock;
    constructor(menu, clock) {
        this.menu = menu;
        this.clock = clock;
    }
    summary() {
        return `${this.clock.now()}: ${this.menu.today().join(', ')}`;
    }
};
OrdersService = __decorate([
    Injectable(),
    __metadata("design:paramtypes", [MenuService,
        Clock])
], OrdersService);
export { OrdersService };
$ node dist/main.js
design:paramtypes of OrdersService: [ 'MenuService', 'Clock' ]
OrdersService.summary(): 10:15: poha, dal rice, thepla

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node run.mjs

Read the compiled class from the top. The constructor’s private readonly menu: MenuService became a field and an assignment, because parameter properties are TypeScript syntax too. The decorator became OrdersService = __decorate([Injectable(), __metadata("design:paramtypes", [MenuService, Clock])], OrdersService): a list of decorator calls applied to the class when the module loads. The second entry of that list is the one Nest depends on.

Checkpoint

Check: What the compiler makes of decorators

  1. Question 1 of 2 What did TypeScript turn @Injectable() on OrdersService into?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: A call, OrdersService = __decorate([Injectable(), __metadata(…)], OrdersService)

    With experimentalDecorators on, tsc compiles each decorator into an entry of a __decorate call. The recorded output shows Injectable() and __metadata("design:paramtypes", [MenuService, Clock]) applied to OrdersService.

  2. Question 2 of 2 What did the compiler write for "constructor(private readonly menu: MenuService, …)"?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A field named menu and an assignment this.menu = menu inside the constructor

    The compiled class reads menu; clock; constructor(menu, clock) { this.menu = menu; this.clock = clock; }. That expansion is also why Node.js's type stripping refuses parameter properties.

emitDecoratorMetadata records what the constructor needs

Types disappear when TypeScript compiles to JavaScript. emitDecoratorMetadata keeps one kind of them for every decorated class: the types of its constructor parameters, written as values that exist at run time, under the design:paramtypes: The metadata key under which TypeScript records the types of a decorated class's constructor parameters, as values that exist at run time: a class, or Object or Function when it cannot name one. key. The reflect-metadata package stores them on the class, and Nest reads them back at start-up. You never load that package yourself in an app: the entry files of @nestjs/common and @nestjs/core import it first, so it is in place before any decorated class of yours is evaluated. Follow the list from the source file to the instance:

Interactive · stage by stage From a typed constructor to the instance Nest builds Step through one class. Each stage is where the list of constructor types lives next; the side branch is the build with emitDecoratorMetadata off.
From a typed constructor to the instance Nest builds, step 7 of 7. Showing kitchen.ts, TypeScript compiler, kitchen.js, reflect-metadata, Nest's container, new OrdersService(…). In focus: no-metadata.metadata off: no argumentskitchen.tsconstructor(menu, clock)TypeScript compileremitDecoratorMetadata onkitchen.js__metadata(design:paramtypes)reflect-metadata[MenuService, Clock]Nest's containerreads the list at start-upnew OrdersService(…)menu and clock passed inclock is undefined: "reading now" fails

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. The source. OrdersService asks for a MenuService and a Clock in its constructor. Those types are TypeScript: JavaScript has no place to keep them, so on their own they vanish at compile time.
  2. The compiler. tsc compiles the class. experimentalDecorators turns @Injectable() into a call to __decorate, and emitDecoratorMetadata asks for one more thing: a record of the constructor parameter types.
  3. The output. kitchen.js now holds __metadata("design:paramtypes", [MenuService, Clock]): the classes themselves, as values that exist when the program runs.
  4. The metadata store. When the module loads, __decorate runs, and reflect-metadata stores that list on the OrdersService class under the key design:paramtypes. Any code can read it back with Reflect.getMetadata().
  5. The container. At start-up Nest reads design:paramtypes for every provider in the module. It finds MenuService and Clock among the module's providers and builds them first.
  6. The instance. Nest calls new OrdersService(menu, clock) with the two instances, and summary() returns "10:15: poha, dal rice, thepla", as the recorded run printed.
  7. Without the metadata. Built with emitDecoratorMetadata off, kitchen.js has no __metadata line. Nest finds no parameter list, calls new OrdersService() with nothing, and starts without a word; summary() then throws a TypeError.

That is the whole mechanism of constructor injection: no configuration names MenuService or Clock as the dependencies of OrdersService; the compiler wrote them down. Here is the same program built with emitDecoratorMetadata switched off in tsconfig.no-metadata.json:

The same program without decorator metadata JavaScript · compiled/no-metadata.mjs
import { session } from '../session.mjs';

// The same program, built with emitDecoratorMetadata switched off (tsconfig.no-metadata.json).
session(`
npx tsc -p tsconfig.no-metadata.json
sed -n '/^OrdersService = /,$p' dist-no-metadata/kitchen.js
node dist-no-metadata/main.js
`);

Output

$ npx tsc -p tsconfig.no-metadata.json
$ sed -n '/^OrdersService = /,$p' dist-no-metadata/kitchen.js
OrdersService = __decorate([
    Injectable()
], OrdersService);
export { OrdersService };
$ node dist-no-metadata/main.js
design:paramtypes of OrdersService: undefined
OrdersService.summary() threw TypeError: Cannot read properties of undefined (reading 'now')

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node no-metadata.mjs

The __metadata line is gone, Reflect.getMetadata finds nothing, and Nest starts without a warning: with no list of parameter types it calls new OrdersService() with no arguments. The failure waits until summary() touches this.clock. That delay is what makes missing decorator metadata: Type information that TypeScript writes next to a decorated class when emitDecoratorMetadata is on, such as design:paramtypes. Libraries read it with the reflect-metadata package. expensive to debug: the error points at the line that used the dependency, not at the build setting that lost it.

Key idea

Nest injects by the parameter types that the compiler records next to each decorated class. If the list is missing, Nest builds the class with nothing and the error shows up later, at first use.

Checkpoint

Check: design:paramtypes and injection

  1. Question 1 of 2 What does design:paramtypes hold for OrdersService?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The classes MenuService and Clock, the types of its constructor parameters

    emitDecoratorMetadata writes the types of a decorated class's constructor parameters as run-time values, here the two classes, and Nest looks each one up among the module's providers.

  2. Question 2 of 2 With emitDecoratorMetadata off, what happened when Nest built OrdersService?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Nest started without a warning, and summary() later threw a TypeError

    The recorded run printed design:paramtypes undefined, then "OrdersService.summary() threw TypeError: Cannot read properties of undefined (reading 'now')".

Node.js’s type stripping cannot run Nest source

Node.js 24 runs .ts files directly: type stripping: Running TypeScript by deleting its type annotations instead of compiling it, as Node.js does for .ts files. Syntax that needs a transformation, such as decorators or parameter properties, cannot run that way. is on by default since Node.js 23.6 and stable since 24.12. It works by deleting type annotations and running what is left, which is fine for plain TypeScript and fatal for Nest. Each of these three runs is meant to fail:

Nest source under Node.js's type stripping JavaScript · strip/run.mjs
import { session } from '../session.mjs';

// Node.js runs .ts files by deleting their types. Each of these runs is meant to fail; read which line stops it.
session(`
!node src/kitchen.ts
!node src/menu.controller.ts
!node --experimental-transform-types src/kitchen.ts
`);

Output

$ node src/kitchen.ts
node:internal/modules/run_main:107
    triggerUncaughtException(
    ^

src/kitchen.ts:12
export class OrdersService {
  constructor(private readonly clock: Clock) {}
                               ^^^^^^^^^^^^

SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript parameter property is not supported in strip-only mode
    at parseTypeScript (node:internal/modules/typescript:68:40)
    at processTypeScriptCode (node:internal/modules/typescript:146:42)
    at stripTypeScriptModuleTypes (node:internal/modules/typescript:215:22)
    at ModuleLoader.<anonymous> (node:internal/modules/esm/translators:680:29)
    at #translate (node:internal/modules/esm/loader:438:20)
    at afterLoad (node:internal/modules/esm/loader:506:29)
    at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:511:12)
    at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:564:36)
    at afterResolve (node:internal/modules/esm/loader:611:52)
    at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:617:12) {
  code: 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX'
}

Node.js v24.21.0
[exit status 1]
$ node src/menu.controller.ts
src/menu.controller.ts:3
@Controller('menu')
^

SyntaxError: Invalid or unexpected token
    at compileSourceTextModule (node:internal/modules/esm/utils:319:16)
    at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:89:18)
    at ModuleLoader.<anonymous> (node:internal/modules/esm/translators:681:10)
    at #translate (node:internal/modules/esm/loader:438:20)
    at afterLoad (node:internal/modules/esm/loader:506:29)
    at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:511:12)
    at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:564:36)
    at afterResolve (node:internal/modules/esm/loader:611:52)
    at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:617:12)
    at node:internal/modules/esm/loader:636:32

Node.js v24.21.0
[exit status 1]
$ node --experimental-transform-types src/kitchen.ts
(node:<pid>) ExperimentalWarning: Transform Types is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
src/kitchen.ts:2
@Injectable()
^

SyntaxError: Invalid or unexpected token
    at compileSourceTextModule (node:internal/modules/esm/utils:319:16)
    at ModuleLoader.moduleStrategy (node:internal/modules/esm/translators:89:18)
    at ModuleLoader.<anonymous> (node:internal/modules/esm/translators:681:10)
    at #translate (node:internal/modules/esm/loader:438:20)
    at afterLoad (node:internal/modules/esm/loader:506:29)
    at ModuleLoader.loadAndTranslate (node:internal/modules/esm/loader:511:12)
    at #getOrCreateModuleJobAfterResolve (node:internal/modules/esm/loader:564:36)
    at afterResolve (node:internal/modules/esm/loader:611:52)
    at ModuleLoader.getOrCreateModuleJob (node:internal/modules/esm/loader:617:12)
    at node:internal/modules/esm/loader:636:32

Node.js v24.21.0
[exit status 1]

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node run.mjs

The first run stops at private readonly clock: Clock, a parameter property, which needs code generated rather than types deleted (ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX). The second file has no parameter property, and it stops at the @ of @Controller('menu'): deleting types leaves the decorator in place, and V8 cannot parse decorators yet. The third run adds --experimental-transform-types, which does generate code for parameter properties, and still stops at the @, because Node.js does not transform decorators at all.

TypeScript syntax Type stripping With --experimental-transform-types
Type annotations, interfaces, import type runs runs
Parameter properties, enum fails runs
Decorators fails fails
emitDecoratorMetadata (a tsconfig.json option) not applied not applied

Even if V8 parsed decorators, the last row would remain: Node.js does not read tsconfig.json, so nothing would write design:paramtypes. A Nest project always compiles first, with tsc through nest build, or with SWC, which the NestJS documentation describes as about 20 times faster. nest start -b swc uses it, and SWC writes the same metadata once its configuration turns on legacyDecorator and decoratorMetadata; it does not check types, so keep tsc running as well, with --type-check or in CI.

Checkpoint

Check: Why type stripping cannot run Nest

  1. Question 1 of 2 Why did node src/menu.controller.ts fail on Node.js 24.21.0, although the file has no parameter property?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Type stripping leaves decorators in the code, and V8 cannot parse @Controller('menu') yet

    The run printed "SyntaxError: Invalid or unexpected token" under @Controller('menu'). Node.js deletes type annotations and runs the rest, and the rest still contains a decorator.

  2. Question 2 of 2 Does node --experimental-transform-types make kitchen.ts run?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: No; it handles the parameter property but still stops at the decorator

    The third run printed the experimental warning, then "SyntaxError: Invalid or unexpected token" under @Injectable(). Nest source needs a real compile step: tsc through nest build, or SWC.

Three ways to lose the metadata, and the fix for each

The compiler writes a parameter’s type as a value only when it can name one. Three common changes leave it nothing to name. This program tries five services that each need a clock: three with a mistake, two with the fix:

traps/src: five services that need a clock

TypeScript (services.ts) · traps/src/services.ts

import { Injectable } from '@nestjs/common';
import type { Clock as ClockType } from './clock.js';
import { Clock, ClockLike, type ClockShape } from './clock.js';

// 1. The class is imported with "import type": only its type survives compilation.
@Injectable()
export class ImportTypeService {
  constructor(private readonly clock: ClockType) {}
  cutOff() {
    return `orders close at ${this.clock.now()}`;
  }
}

// 1, fixed: a normal import keeps the class, so the compiler can record it.
@Injectable()
export class ImportValueService {
  constructor(private readonly clock: Clock) {}
  cutOff() {
    return `orders close at ${this.clock.now()}`;
  }
}

// 2. The parameter is typed with an interface.
@Injectable()
export class InterfaceService {
  constructor(private readonly clock: ClockShape) {}
  cutOff() {
    return `orders close at ${this.clock.now()}`;
  }
}

// 2, fixed: an abstract class is the token; the module says which class provides it.
@Injectable()
export class AbstractClassService {
  constructor(private readonly clock: ClockLike) {}
  cutOff() {
    return `orders close at ${this.clock.now()}`;
  }
}

// 3. No @Injectable(): the compiler records nothing for an undecorated class.
export class UndecoratedService {
  constructor(private readonly clock: Clock) {}
  cutOff() {
    return `orders close at ${this.clock.now()}`;
  }
}

TypeScript (clock.ts) · traps/src/clock.ts

import { Injectable } from '@nestjs/common';

@Injectable()
export class Clock {
  now() {
    return '10:15';
  }
}

// An abstract class exists at run time, so it can stand for "any clock" as an injection token.
export abstract class ClockLike {
  abstract now(): string;
}

// An interface is erased by the compiler: at run time there is nothing to look up.
export interface ClockShape {
  now(): string;
}

TypeScript (main.ts) · traps/src/main.ts

import 'reflect-metadata';
import { Module, type Provider } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Clock, ClockLike } from './clock.js';
import { AbstractClassService, ImportTypeService, ImportValueService, InterfaceService, UndecoratedService } from './services.js';

type Service = { name: string; prototype: { cutOff(): string } };

async function attempt(service: Service, providers: Provider[] = [Clock]) {
  const types: Function[] | undefined = Reflect.getMetadata('design:paramtypes', service);
  console.log(`${service.name}: design:paramtypes ${types ? `[${types.map((t) => t.name).join(', ')}]` : 'not recorded'}`);
  @Module({ providers: [...providers, service as never] })
  class TrialModule {}
  try {
    const app = await NestFactory.createApplicationContext(TrialModule, { logger: false, abortOnError: false });
    try {
      console.log(`  cutOff(): ${app.get<{ cutOff(): string }>(service as never).cutOff()}`);
    } catch (error) {
      console.log(`  Nest started, then cutOff() threw ${(error as Error).name}: ${(error as Error).message}`);
    }
    await app.close();
  } catch (error) {
    // The message, then the first two of the solutions Nest suggests (line 1 is the heading "Potential solutions:").
    const lines = (error as Error).message.split('\n').filter((l) => l.trim());
    console.log(`  Nest refused to start: ${(error as Error).constructor.name}`);
    for (const line of [lines[0], lines[2], lines[3]]) console.log(`  ${line}`);
  }
}

await attempt(ImportTypeService);
await attempt(ImportValueService);
await attempt(InterfaceService);
await attempt(AbstractClassService, [{ provide: ClockLike, useClass: Clock }]);
await attempt(UndecoratedService);
Three mistakes and two fixes, as Nest reports them JavaScript · traps/run.mjs
import { session } from '../session.mjs';

// Five services that ask for a clock: three mistakes that erase the metadata, and two fixes.
session(`
npx tsc -p tsconfig.json
node dist/main.js
`);

Output

$ npx tsc -p tsconfig.json
$ node dist/main.js
ImportTypeService: design:paramtypes [Function]
  Nest refused to start: UnknownDependenciesException
  Nest can't resolve dependencies of the ImportTypeService (?). Please make sure that the argument Function at index [0] is available in the TrialModule module.
  - Is TrialModule a valid NestJS module?
  - If Function is a provider, is it part of the current TrialModule?
ImportValueService: design:paramtypes [Clock]
  cutOff(): orders close at 10:15
InterfaceService: design:paramtypes [Object]
  Nest refused to start: UnknownDependenciesException
  Nest can't resolve dependencies of the InterfaceService (?). Please make sure that the argument at index [0] is available in the current module.
  - The dependency at index [0] appears to be undefined at runtime
  - This commonly occurs when using 'import type' instead of 'import' for injectable classes
AbstractClassService: design:paramtypes [ClockLike]
  cutOff(): orders close at 10:15
UndecoratedService: design:paramtypes not recorded
  Nest started, then cutOff() threw TypeError: Cannot read properties of undefined (reading 'now')

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node run.mjs

Mistake What the compiler wrote What Nest did The fix
import type { Clock } [Function] refused to start: no provider called Function a normal import { Clock }
Parameter typed by an interface [Object] refused to start, suggesting import type an abstract class, or @Inject() with a token
No @Injectable() on the class nothing at all started, then cutOff() threw a TypeError add @Injectable()

Two lines of Nest’s messages are misleading, and worth remembering. For import type, Nest asks you to check whether a provider named Function is in the module. For the interface, it suggests that you replaced import with import type. In both cases the real cause is that the type had no run-time value. An interface can never be an injection token: The key Nest's container looks up to find a provider: a class, a string or a symbol. An interface cannot be one, because it does not exist at run time.: use an abstract class, which exists at run time and can be provided with useClass, or a string or symbol token with @Inject().

Cleaning up imports with “import type”

Linters and editors offer to turn imports that are only used as types into import type. A constructor parameter of an injectable class looks like a type-only use, but the compiler needs the class itself for the metadata. Keep import { Clock } for every class that a decorated constructor receives.

Checkpoint

Check: Mistakes that erase the metadata

  1. Question 1 of 2 A service has "import type { Clock } from './clock.js'" and "constructor(private readonly clock: Clock)". What do you change?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Make it a normal import, import { Clock }, so the compiler can record the class

    The recorded run shows design:paramtypes [Function] and "the argument Function at index [0]" for the import type version, and orders close at 10:15 once Clock is imported normally.

  2. Question 2 of 2 A service's constructor takes a ClockShape, which is an interface. What is the most direct fix?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Use an abstract class as the type and provide it with { provide: ClockLike, useClass: Clock }

    The interface version recorded [Object] and failed; the abstract-class version recorded [ClockLike] and printed orders close at 10:15. A string or symbol token with @Inject() works too.

TypeScript 6 or 7 for a Nest 12 project

npm’s latest TypeScript is 7.0.2, the new native compiler, while Nest 12’s tools stay on 6. The lesson’s package.json installs both, 7.0.2 under the name typescript-7, so this session can compile kitchen.ts with each and then ask each package what it exports:

versions/api.mjs JavaScript · versions/api.mjs
// Asks each installed TypeScript what its main entry exports. Nest's CLI plugins and the schematics behind
// nest generate import the compiler API from 'typescript': createSourceFile, SyntaxKind and the rest.
import ts6 from 'typescript';
import * as ts7 from 'typescript-7';

console.log(`typescript ${ts6.version}: createSourceFile is a ${typeof ts6.createSourceFile}, SyntaxKind is an ${typeof ts6.SyntaxKind}`);
const names = Object.keys(ts7).filter((name) => name !== 'default' && name !== 'module.exports');
console.log(`typescript ${ts7.version}: its main entry exports only ${names.join(' and ')}`);
The same file through TypeScript 6.0.3 and 7.0.2 JavaScript · versions/run.mjs
import { session } from '../session.mjs';

// The lesson's package.json installs TypeScript 7.0.2 as "typescript-7" beside 6.0.3. Both compile the same file;
// then each is asked for the compiler API that Nest's tooling uses.
session(`
npx tsc --version
node ../node_modules/typescript-7/bin/tsc --version
npx tsc -p ../compiled/tsconfig.json --outDir dist-6
node ../node_modules/typescript-7/bin/tsc -p ../compiled/tsconfig.json --outDir dist-7
grep -A 1 'design:paramtypes' dist-6/kitchen.js dist-7/kitchen.js
node api.mjs
`);

Output

$ npx tsc --version
Version 6.0.3
$ node ../node_modules/typescript-7/bin/tsc --version
Version 7.0.2
$ npx tsc -p ../compiled/tsconfig.json --outDir dist-6
$ node ../node_modules/typescript-7/bin/tsc -p ../compiled/tsconfig.json --outDir dist-7
$ grep -A 1 'design:paramtypes' dist-6/kitchen.js dist-7/kitchen.js
dist-6/kitchen.js:    __metadata("design:paramtypes", [MenuService,
dist-6/kitchen.js-        Clock])
--
dist-7/kitchen.js:    __metadata("design:paramtypes", [MenuService,
dist-7/kitchen.js-        Clock])
$ node api.mjs
typescript 6.0.3: createSourceFile is a function, SyntaxKind is an object
typescript 7.0.2: its main entry exports only version and versionMajorMinor

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node run.mjs

Both compilers wrote the same design:paramtypes list, so an app compiled by either runs the same. The difference is in the last two lines. Nest’s CLI plugins and the schematics behind nest generate call TypeScript’s compiler API, createSourceFile, SyntaxKind and the rest, from the typescript package. TypeScript 6.0.3 exports it; 7.0.2’s main entry exports only its version. That is why @nestjs/cli 12.0.8 depends on typescript ~6.0.2, and why the Swagger and GraphQL packages declare ^5.5.0 || ^6.0.0 as their peer.

Standard decorators are not an option yet

TypeScript 5.0 added the decorators of the TC39 proposal, which need no experimentalDecorators flag. Its release notes state that they are not compatible with emitDecoratorMetadata and cannot decorate parameters, and Nest needs both, for constructor injection and for @Param(), @Body() and their kind. Nest 12 projects keep experimentalDecorators: true.

Keep a Nest 12 project on TypeScript 6, the version its package.json pins (^6.0.2 in the starter), and let a newer major arrive through the CLI’s own dependency.

Checkpoint

Check: Which TypeScript for Nest 12

  1. Question 1 of 2 Did TypeScript 7.0.2 write different decorator metadata from TypeScript 6.0.3 for kitchen.ts?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: No, both wrote __metadata("design:paramtypes", [MenuService, Clock])

    The new native compiler keeps experimentalDecorators and emitDecoratorMetadata, and its output for this file has the same design:paramtypes list.

  2. Question 2 of 2 Then why should a Nest 12 project stay on TypeScript 6?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Nest's CLI plugins and schematics use the compiler API, which TypeScript 7.0.2's main entry does not export

    @nestjs/cli 12.0.8 depends on typescript ~6.0.2, and the Swagger and GraphQL packages declare ^5.5.0 || ^6.0.0, because they call the compiler API that TypeScript 7.0.2 does not offer from its main entry.

Practise: predict the metadata

Each question shows a constructor, an import or a build setting. Predict what the compiler writes, or what Nest does with it, and then read why:

Practice that adapts to you

Practise this lesson

10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 10 When does the __decorate call of a compiled class run?

    Choose one answer.

    Show the answer: Question 1 of 10

    Answer: Once, when the module that defines the class is loaded

    The compiled file assigns OrdersService = __decorate([...], OrdersService) right after the class, so the decorators and the metadata are in place as soon as the file has been loaded.

  2. Question 2 of 10 Why can a framework read decorators only after a compile step, in a JavaScript engine of today?

    Choose one answer.

    Show the answer: Question 2 of 10

    Answer: Engines do not parse decorator syntax yet, so a compiler must turn it into function calls

    Node.js's own documentation says decorators are not transformed by type stripping and will not be supported until JavaScript supports them natively. Until then a compiler rewrites them.

  3. Question 3 of 10 A decorated constructor has the parameter "name: string". What does design:paramtypes hold for it?

    Choose one answer.

    Show the answer: Question 3 of 10

    Answer: String, the built-in constructor, which Nest cannot inject without a token

    TypeScript records string as String, number as Number and so on. Such a parameter needs @Inject() with a token, as the providers lessons show.

  4. Question 4 of 10 After a build-tool change, a service's dependency is undefined at run time, but the app starts without errors. Which cause fits best?

    Choose one answer.

    Show the answer: Question 4 of 10

    Answer: The new build no longer writes decorator metadata

    Of the failures in this lesson, only missing metadata (no emitDecoratorMetadata, or no decorator on the class) lets the app start and fail later. Check the compiled output for a __metadata("design:paramtypes", …) line.

  5. Question 5 of 10 Which error did node src/kitchen.ts report first?

    Choose one answer.

    Show the answer: Question 5 of 10

    Answer: ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, for the parameter property

    The recorded run printed "SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript parameter property is not supported in strip-only mode".

  6. Question 6 of 10 If V8 one day parses decorators, could Node.js's type stripping run Nest source files then?

    Choose one answer.

    Show the answer: Question 6 of 10

    Answer: Not as it works now, because Node.js does not read tsconfig.json, so nothing writes design:paramtypes

    Two things are missing under type stripping: decorators that parse, and the metadata that emitDecoratorMetadata writes. Node.js's documentation states that it does not support features that depend on tsconfig.json.

  7. Question 7 of 10 What did the compiler record for UndecoratedService, the class without @Injectable()?

    Choose one answer.

    Show the answer: Question 7 of 10

    Answer: Nothing; design:paramtypes was not recorded

    The run printed "UndecoratedService: design:paramtypes not recorded", then a TypeError from cutOff(). Add @Injectable() to every class that Nest constructs with arguments.

  8. Question 8 of 10 Nest says "the argument Function at index [0]" is not available in the module. What do you look for first?

    Choose one answer.

    Show the answer: Question 8 of 10

    Answer: An import type of the class that the constructor asks for

    Read the type the message names: Function points to import type of a class, Object to an interface or a type that cannot be named, and no error at start-up to missing metadata.

  9. Question 9 of 10 Which TypeScript range does @nestjs/cli 12.0.8 depend on?

    Choose one answer.

    Show the answer: Question 9 of 10

    Answer: ~6.0.2

    @nestjs/cli 12.0.8 depends on typescript ~6.0.2, and the starter's own devDependencies ask for ^6.0.2.

  10. Question 10 of 10 Why can Nest 12 not switch to TypeScript's standard decorators, the ones that need no experimentalDecorators flag?

    Choose one answer.

    Show the answer: Question 10 of 10

    Answer: They do not support emitDecoratorMetadata or parameter decorators, and Nest needs both

    Constructor injection depends on design:paramtypes, and parameter decorators mark @Param(), @Body() and their kind. The standard decorators of TypeScript 5.0 offer neither, so Nest keeps experimentalDecorators.

Interview questions

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid · Warm-up

    What is design:paramtypes, and who writes it and reads it in a Nest app?

    It is decorator metadata: the list of a decorated class's constructor parameter types, kept as values that exist at run time. The TypeScript compiler writes it when emitDecoratorMetadata is on, as a __metadata("design:paramtypes", [MenuService, Clock]) entry in the class's __decorate call, and the reflect-metadata package stores it on the class when the file loads. Nest reads it at start-up with Reflect.getMetadata and injects one provider per entry. Interfaces become Object, an import type of a class becomes Function, and an undecorated class gets no list at all, which is why those three mistakes break injection.

  2. Mid to senior

    After a refactor, one Nest service fails at start-up because Nest cannot resolve "the argument Function at index [0]". After a build change, another service starts but its dependency is undefined at first use. Explain both failures, and how you would find the cause of each.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    Your team wants to drop the build step and run a NestJS 12 app with Node.js 24's built-in TypeScript support, to speed up development. Explain why it fails, and what you would offer instead.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A TypeScript compiler turns Nest’s decorators into __decorate calls; with emitDecoratorMetadata it also writes the constructor’s parameter types under design:paramtypes.
  • Nest reads that list at start-up to decide what to inject. Without it, Nest builds the class with no arguments and the error appears at first use.
  • Node.js’s type stripping cannot run Nest source: parameter properties need a transform, decorators do not parse, and Node.js never reads tsconfig.json.
  • import type of a class, an interface as a parameter type and a missing @Injectable() all erase the metadata; use a normal import, an abstract class or a token, and the decorator.
  • TypeScript 7.0.2 writes the same metadata but has no compiler API in its main entry, so Nest 12’s tools stay on TypeScript 6.

Terms in this lesson

decorator
A function applied with @ to a class, method, property or parameter, which can record data about it or change it. Nest uses TypeScript's experimental decorators, which a compiler turns into function calls.
decorator metadata
Type information that TypeScript writes next to a decorated class when emitDecoratorMetadata is on, such as design:paramtypes. Libraries read it with the reflect-metadata package.
design:paramtypes
The metadata key under which TypeScript records the types of a decorated class's constructor parameters, as values that exist at run time: a class, or Object or Function when it cannot name one.
injection token
The key Nest's container looks up to find a provider: a class, a string or a symbol. An interface cannot be one, because it does not exist at run time.
type stripping
Running TypeScript by deleting its type annotations instead of compiling it, as Node.js does for .ts files. Syntax that needs a transformation, such as decorators or parameter properties, cannot run that way.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.