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: trueand 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 case | Use a… | Why |
|---|---|---|
| DTO with validation | class | Must exist at runtime for class-validator |
| Injectable service | class | Used as a DI token |
| Pure shape / config contract | interface | Compile-time only, zero output |
| Generic repository contract | interface | Describes 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
experimentalDecoratorsandemitDecoratorMetadataenabled — Nest will not run without them. - Enable
strictNullChecks(ideally fullstrict) 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
ValidationPipeusingwhitelist: true. - Prefer generics over
anyfor shared services, pipes, and repositories.