Skip to content
TypeScript ts nestjs 4 min read

TypeScript in NestJS

NestJS is a server-side framework written in TypeScript, and it leans on the language far more aggressively than most Node frameworks. Where Express treats types as an optional add-on, Nest makes TypeScript features the mechanism by which the framework operates: classes become injectable providers, decorators attach metadata that the runtime reads, and generics keep services and repositories reusable without losing type safety. Understanding which TS features Nest depends on — and why some patterns (like classes over interfaces for DTOs) are mandatory rather than stylistic — is the key to writing idiomatic Nest code. This page is a map of those features and links to the deeper pages that follow.

The compiler options Nest requires

Nest depends on legacy (“experimental”) decorators and emitted decorator metadata. A generated project ships with these already enabled, and they are non-negotiable for the framework to function.

// tsconfig.json
{
  "compilerOptions": {
    "experimentalDecorators": true,   // enable @Decorator syntax (legacy/Stage 2)
    "emitDecoratorMetadata": true,    // emit type info via reflect-metadata
    "target": "ES2021",
    "module": "commonjs",
    "moduleResolution": "node",
    "strictNullChecks": true
  }
}

emitDecoratorMetadata is what makes constructor-based dependency injection work without you naming tokens: the compiler emits each parameter’s type under the design:paramtypes key, and Nest reads it at runtime with reflect-metadata. Without it, the container would not know what to inject.

Nest uses the legacy decorator implementation, not the TS 5.0 Stage 3 standard. The two are not interchangeable — keep experimentalDecorators: true and do not migrate Nest code to standard decorators yet.

Decorators describe your application

Almost every Nest building block is a class annotated with a decorator. The decorator does not change the class behavior directly; it attaches metadata that Nest’s runtime later inspects to wire everything together.

import { Module, Controller, Get, Injectable } from "@nestjs/common";

@Injectable()
export class CatsService {
  findAll(): string[] {
    return ["Tom", "Felix"];
  }
}

@Controller("cats")
export class CatsController {
  constructor(private readonly cats: CatsService) {}

  @Get()
  findAll(): string[] {
    return this.cats.findAll();
  }
}

@Module({ controllers: [CatsController], providers: [CatsService] })
export class CatsModule {}

@Controller, @Get, @Injectable, and @Module are all metadata carriers. See Decorators in NestJS for the full catalogue and how to build your own.

Dependency injection through constructor types

Nest’s DI container resolves dependencies from constructor parameter types. Because the compiler emits those types as metadata, you rarely write an explicit token — the class itself is the token.

@Injectable()
export class ReportService {
  constructor(
    private readonly cats: CatsService,   // resolved by type
    private readonly logger: Logger,
  ) {}
}

The private readonly syntax here is a TypeScript parameter property: it declares and assigns the field in one step. This is core OOP that Nest reuses everywhere; the underlying principle is covered in Dependency Injection Basics.

Classes vs interfaces: a runtime distinction

TypeScript interfaces vanish at compile time, so Nest cannot inject or validate against them. Anything Nest must reference at runtime — DTOs, providers, entities — has to be a class. Interfaces remain useful for compile-time-only contracts.

Use caseUse a…Why
DTO with validationclassMust exist at runtime for class-validator
Injectable serviceclassUsed as a DI token
Pure shape / config contractinterfaceCompile-time only, zero output
Generic repository contractinterfaceDescribes a shape, not injected directly

This trade-off is important enough to have its own page: Interfaces vs Classes in NestJS.

Generics for reusable infrastructure

Generic services, repositories, and pipes let you write infrastructure once and reuse it across entity types without any. Nest’s own ParseIntPipe, ConfigService<T>, and the TypeORM Repository<Entity> are all generic.

@Injectable()
export class GenericService<T extends { id: number }> {
  private items: T[] = [];

  findOne(id: number): T | undefined {
    return this.items.find((item) => item.id === id);
  }
}

Generics in NestJS goes deeper into typed repositories and pipes.

Validation at the boundary

Incoming JSON is untyped at runtime. Nest pairs class-validator decorators on DTO classes with a global ValidationPipe to validate and transform requests before they reach a handler.

import { ValidationPipe } from "@nestjs/common";

app.useGlobalPipes(
  new ValidationPipe({ whitelist: true, transform: true }),
);

See Validation with class-validator for whitelist, transform, and decorator usage.

Best Practices

  • Keep experimentalDecorators and emitDecoratorMetadata enabled — Nest will not run without them.
  • Enable strictNullChecks (ideally full strict) so DTOs and services model optionality honestly.
  • Use classes for anything Nest touches at runtime (DTOs, providers); reserve interfaces for compile-time contracts.
  • Let the DI container resolve dependencies by type; only reach for explicit tokens when injecting non-class values.
  • Centralize validation with a single global ValidationPipe using whitelist: true.
  • Prefer generics over any for shared services, pipes, and repositories.
Last updated June 29, 2026
Was this helpful?