| |

Node.js 38 🟢 NestJS Modules, Controllers, Services, and Dependency Injection

NestJS’s architecture rests on four interlocking concepts: modules, controllers, services, and dependency injection. A module is the organizational boundary. A controller is the HTTP entry point. A service contains the business logic. Dependency injection is the mechanism that connects them without hardcoding the relationships. Together, they form a structure where every piece has a defined role, and the boundaries between them are enforced by the framework rather than by convention.

The dependency injection container is what makes the structure work. When a controller declares a service as a constructor parameter, the container resolves the dependency, creates or retrieves the instance, and injects it. The controller never calls new on the service. It never imports the service’s implementation details. It only declares the type, and the container supplies the instance. This inversion of control is the foundation of testability: in a test, the container can be overridden to supply a mock instead of the real service, and the controller code does not change. The service’s constructor is also subject to injection, so a service can depend on other services, repositories, or configuration objects, and the container resolves the entire graph at startup.

The module system provides the boundaries. A module declares its controllers, its providers, and what it exports to other modules. A provider that is not exported is private to the module. A module that imports another module gains access only to the exported providers, not the private ones. This encapsulation prevents the accidental coupling that plagues larger Node.js applications, where any file can import any other file and the dependency graph becomes a tangle. In NestJS, the dependency graph is explicit: the imports and exports arrays are the contract, and the container enforces it .

This chapter covers three areas. First, why modules, controllers, and services exist as separate concepts — the separation-of-concerns problem and the encapsulation boundary that modules provide. Second, how each building block works — the @Module() decorator’s four keys, the controller’s routing and parameter decorators, and the service’s @Injectable() marker. Third, how dependency injection connects them — constructor injection, the resolution process, provider scopes, and the custom provider forms that handle non-class dependencies. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the module graph and the injection flow.

Key point: A module declares its controllers, providers, and exports. A controller handles HTTP requests and delegates to services. A service contains business logic and is marked with @Injectable(). The dependency injection container resolves the graph at startup: a class lists its dependencies in the constructor, and the container supplies the instances. Providers are singletons by default, shared across the application .


Why modules, controllers, and services exist

The separation-of-concerns problem. A single class that handles HTTP routing, validates input, contains business logic, and accesses the database is difficult to test, difficult to change, and difficult to understand. The NestJS architecture separates these concerns. The controller’s job is to receive the request, extract the data, delegate to a service, and return the result. The service’s job is to contain the business logic. The module’s job is to declare the boundary and wire the pieces together. Each piece has one reason to change, and the changes are isolated from each other .

The encapsulation problem. In a typical Node.js application, any file can import any other file. A utility function defined in one feature can be imported by every other feature, and the dependency graph becomes impossible to reason about. NestJS modules provide encapsulation. A provider declared in a module’s providers array is private to that module unless it is explicitly exported. A module that wants to use the provider must import the module that owns it. This creates a visible, enforceable dependency graph. The exports array is the module’s public API, and everything else is an implementation detail .

The dependency-management problem. A service that needs a database connection, a logger, and a configuration object must acquire each one. Without a container, the service either creates them itself — coupling it to their construction — or receives them from the caller, which pushes the responsibility up the call stack. NestJS’s dependency injection container solves this. The service declares its dependencies in the constructor, and the container creates or retrieves them. The service does not know how the dependencies are constructed, only that they will be available. This makes the service testable: the container can be configured to supply mocks, and the service’s code does not change .

The singleton problem. A database connection pool should be created once and shared. A service that creates its own pool creates a new pool for every instance. NestJS’s container manages lifetimes. By default, a provider is a singleton: the container creates one instance and injects it wherever it is needed. This means a DatabaseService is instantiated once, and every service that depends on it receives the same instance. The container manages the lifecycle, including cleanup on shutdown .

The testability problem. A class that creates its own dependencies is difficult to test. The test must set up the dependencies, which may require a database, a network connection, or a file system. A class that receives its dependencies through injection is easy to test. The test provides a mock for each dependency, and the class under test does not know the difference. NestJS’s Test.createTestingModule() builds a container specifically for testing, where providers can be overridden with mocks. The same class that runs in production with real dependencies runs in tests with mocks, and the code is identical .

The trade-off. The module system and dependency injection introduce indirection. A controller does not show where the service comes from; the service does not show where its dependencies come from. The connections are made by the container, and the container reads metadata that is generated by decorators. This indirection is the cost of the structure. For a small application, the cost may outweigh the benefit. For an application that will grow, the structure pays for itself: new features follow the same pattern, boundaries are enforced, and testing is straightforward.


a. Modules — the organizational boundary

A module is a class annotated with @Module(). The decorator’s object has four keys: imports, controllers, providers, and exports .

@Module({
  imports: [DatabaseModule],
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}

The controllers array declares the controllers that belong to the module. The providers array declares the services, repositories, and other injectable classes that the module owns. The exports array declares which of those providers are visible to other modules. A provider that is not exported is private — other modules cannot inject it even if they import the module .

The imports array declares the modules whose exported providers this module needs. When UsersModule imports DatabaseModule, the UsersService can inject DatabaseService if DatabaseModule exports it. If DatabaseModule does not export it, the injection fails at startup .

The root module, AppModule, imports the feature modules. A well-designed AppModule is thin: it declares the feature modules and nothing else. A module that declares every controller and provider in the application is a sign that the boundaries are wrong .

Feature modules are the standard pattern. One module per resource or domain: UsersModule, ProductsModule, OrdersModule. Each module bundles its controller, service, DTOs, and any supporting providers. The module directory contains all the files for that feature, and the module file declares the boundary .


b. Controllers — the HTTP entry point

A controller is a class annotated with @Controller(). The decorator takes an optional route prefix. The class contains methods annotated with HTTP method decorators: @Get(), @Post(), @Put(), @Delete(), @Patch() .

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll(@Query() query: QueryUsersDto) {
    return this.usersService.findAll(query);
  }

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.findOne(id);
  }

  @Post()
  @HttpCode(HttpStatus.CREATED)
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }
}

The controller’s job is to extract the request data and delegate to the service. It should not contain business logic, database queries, or manual validation. The @Body(), @Query(), @Param(), and @Headers() decorators extract the corresponding parts of the request. Pipes like ParseIntPipe validate and transform the extracted values .

The @HttpCode() decorator sets the response status code. By default, a POST returns 201 and other methods return 200. The @Header() decorator sets response headers. The @Res() decorator injects the raw response object, but using it opts out of NestJS’s standard response handling, so it should be avoided unless necessary .

Controllers are thin by design. A controller method that contains an if statement about business rules is a sign that the logic belongs in a service. The controller should read like a table of contents: each method corresponds to an endpoint, and each method delegates to the service .


c. Services and dependency injection

A service is a class annotated with @Injectable(). It contains the business logic, data access, or any functionality that is not directly tied to HTTP. The @Injectable() decorator attaches metadata that the container uses to resolve the dependency .

@Injectable()
export class UsersService {
  private readonly users: User[] = [];

  findAll(query: QueryUsersDto): User[] {
    return this.users.filter(/* ... */);
  }

  findOne(id: number): User {
    const user = this.users.find((u) => u.id === id);
    if (!user) {
      throw new NotFoundException(`User with id ${id} not found`);
    }
    return user;
  }

  create(dto: CreateUserDto): User {
    const user: User = { id: this.nextId++, ...dto };
    this.users.push(user);
    return user;
  }
}

The service is registered in the module’s providers array. The container instantiates it — by default, as a singleton — and injects it into any class that declares it as a constructor parameter .

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}
}

The private readonly usersService: UsersService parameter is a TypeScript parameter property. It declares a member and assigns the constructor argument to it. The type annotation is the token the container uses to resolve the dependency. The container reads the emitted metadata and supplies the UsersService instance .

The container resolves the entire graph at startup. If UsersService depends on DatabaseService, and DatabaseService depends on ConfigService, the container creates ConfigService first, then DatabaseService, then UsersService, and injects each into the next. If any dependency cannot be resolved — a provider not registered, a circular dependency — the application fails to start, and the error identifies the unresolved token .

Providers are singletons by default. The container creates one instance and reuses it everywhere. This means a DatabaseService is instantiated once, and every service that depends on it receives the same instance. The container also manages cleanup: the onModuleDestroy hook and the onApplicationShutdown hook are called when the application shuts down .

Custom providers handle non-class dependencies. A configuration object, a database connection created by a factory, or a value from an environment variable can be registered with a token and injected using @Inject() .

@Module({
  providers: [
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: async (config: ConfigService) => {
        return await createConnection(config.get('DATABASE_URL'));
      },
      inject: [ConfigService],
    },
  ],
  exports: ['DATABASE_CONNECTION'],
})
export class DatabaseModule {}

The useFactory form creates the value, optionally injecting other providers. The useValue form supplies a ready-made object. The useClass form selects an implementation. The useExisting form creates an alias. These forms are used for configuration objects, database connections, and swappable implementations .


Complete Example Session

// ============================================
// PART 1: THE SERVICE
// ============================================

// users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { User } from './interfaces/user.interface';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Injectable()
export class UsersService {
  private users: User[] = [
    { id: 1, name: 'Alice', email: 'alice@example.com' },
  ];
  private nextId = 2;

  findAll(): User[] {
    return this.users;
  }

  findOne(id: number): User {
    const user = this.users.find((u) => u.id === id);
    if (!user) {
      throw new NotFoundException(`User with id ${id} not found`);
    }
    return user;
  }

  create(dto: CreateUserDto): User {
    const user: User = { id: this.nextId++, ...dto };
    this.users.push(user);
    return user;
  }

  update(id: number, dto: UpdateUserDto): User {
    const user = this.findOne(id);
    Object.assign(user, dto);
    return user;
  }

  remove(id: number): void {
    const index = this.users.findIndex((u) => u.id === id);
    if (index === -1) {
      throw new NotFoundException(`User with id ${id} not found`);
    }
    this.users.splice(index, 1);
  }
}


// ============================================
// PART 2: THE DTOs
// ============================================

// users/dto/create-user.dto.ts
import { IsEmail, IsString, MinLength } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(1)
  name: string;

  @IsEmail()
  email: string;
}

// users/dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

export class UpdateUserDto extends PartialType(CreateUserDto) {}


// ============================================
// PART 3: THE CONTROLLER
// ============================================

// users/users.controller.ts
import {
  Controller,
  Get,
  Post,
  Put,
  Delete,
  Body,
  Param,
  ParseIntPipe,
  HttpCode,
  HttpStatus,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll() {
    return this.usersService.findAll();
  }

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.findOne(id);
  }

  @Post()
  @HttpCode(HttpStatus.CREATED)
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto);
  }

  @Put(':id')
  update(
    @Param('id', ParseIntPipe) id: number,
    @Body() updateUserDto: UpdateUserDto,
  ) {
    return this.usersService.update(id, updateUserDto);
  }

  @Delete(':id')
  @HttpCode(HttpStatus.NO_CONTENT)
  remove(@Param('id', ParseIntPipe) id: number) {
    return this.usersService.remove(id);
  }
}


// ============================================
// PART 4: THE MODULE
// ============================================

// users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService],
})
export class UsersModule {}


// ============================================
// PART 5: THE ROOT MODULE
// ============================================

// app.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from './users/users.module';

@Module({
  imports: [UsersModule],
})
export class AppModule {}


// ============================================
// PART 6: THE BOOTSTRAP
// ============================================

// main.ts
import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

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

  await app.listen(3000);
}
bootstrap();


// ============================================
// PART 7: INJECTING A SERVICE INTO ANOTHER SERVICE
// ============================================

// orders/orders.service.ts
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';

@Injectable()
export class OrdersService {
  constructor(private readonly usersService: UsersService) {}

  findUserOrders(userId: number) {
    const user = this.usersService.findOne(userId);
    return { userId: user.id, orders: [] };
  }
}


// ============================================
// PART 8: THE MODULE THAT IMPORTS ANOTHER MODULE
// ============================================

// orders/orders.module.ts
import { Module } from '@nestjs/common';
import { UsersModule } from '../users/users.module';
import { OrdersController } from './orders.controller';
import { OrdersService } from './orders.service';

@Module({
  imports: [UsersModule],
  controllers: [OrdersController],
  providers: [OrdersService],
})
export class OrdersModule {}


// ============================================
// PART 9: CUSTOM PROVIDER WITH FACTORY
// ============================================

// config/config.module.ts
import { Module } from '@nestjs/common';
import { ConfigService } from './config.service';

@Module({
  providers: [
    {
      provide: 'APP_CONFIG',
      useFactory: () => ({
        port: parseInt(process.env.PORT ?? '3000', 10),
        databaseUrl: process.env.DATABASE_URL ?? 'localhost:5432',
      }),
    },
    ConfigService,
  ],
  exports: ['APP_CONFIG', ConfigService],
})
export class ConfigModule {}


// ============================================
// PART 10: INJECTING A CUSTOM PROVIDER
// ============================================

// users/users.service.ts
import { Injectable, Inject } from '@nestjs/common';

@Injectable()
export class UsersService {
  constructor(
    @Inject('APP_CONFIG') private readonly config: { port: number },
  ) {}

  getPort(): number {
    return this.config.port;
  }
}

The ten parts show the service, the DTOs, the controller, the module, the root module, the bootstrap, injecting a service into another service, importing a module, a custom provider with a factory, and injecting a custom provider.


Quick Reference

Module Keys

KeyPurpose
importsModules whose exports are needed
controllersControllers owned by the module
providersServices, repositories, and injectables
exportsProviders visible to other modules

Controller Decorators

DecoratorPurpose
@Controller('prefix')Route prefix for the controller
@Get(), @Post(), @Put()HTTP method mapping
@Body()Request body
@Query()Query parameters
@Param()Route parameters
@Headers()Request headers
@HttpCode()Response status code
@UseGuards()Bind a guard

Service and Injection

Decorator / APIPurpose
@Injectable()Mark a class as injectable
Constructor parameterDeclare a dependency
@Inject('TOKEN')Inject a custom provider by token
useClass, useValue, useFactory, useExistingCustom provider forms

Provider Scopes

ScopeInstance
Default (Singleton)One per application
RequestOne per request
TransientOne per injection

Best Practices

✅ Do This:

// Use constructor injection
constructor(private readonly usersService: UsersService) {}               // ✅
// Mark services with @Injectable()
@Injectable()
export class UsersService {}                                              // ✅
// Register providers in the module
@Module({ providers: [UsersService] })                                    // ✅
// Export providers other modules need
@Module({ exports: [UsersService] })                                      // ✅
// Keep controllers thin
findOne(@Param('id') id: number) { return this.usersService.findOne(id); }// ✅
// Use DTOs for validation
create(@Body() createUserDto: CreateUserDto) {}                           // ✅

❌ Don’t Do This:

// Don't instantiate services manually
const service = new UsersService();                                       // ❌
// Don't put business logic in controllers
@Get() findAll() { /* database queries, rules, etc. */ }                  // ❌
// Don't forget to export a provider another module needs
@Module({ providers: [UsersService] }) // no exports                       // ❌
// Don't re-declare a provider in two modules
providers: [UsersService] // in two modules → two instances                // ❌
// Don't use interfaces as injection tokens
constructor(private config: AppConfig) {} // AppConfig is an interface      // ❌
// Don't skip validation
create(@Body() body: any) {}                                               // ❌

Common Pitfalls

PitfallWhy It HappensFix
“Nest can’t resolve dependencies”Provider not registered or not exportedCheck providers and exports
Two instances of a singletonProvider re-declared in two modulesDeclare once, import the module
Interface injection failsInterfaces erased at compile timeUse a class or @Inject() with a token
Controller bloatedBusiness logic in controllerMove logic to a service
Validation not appliedValidationPipe not registeredRegister globally or per-route
Circular dependencyTwo modules import each otherUse forwardRef or restructure

Real-World Examples

1. Feature Module

@Module({ controllers: [UsersController], providers: [UsersService], exports: [UsersService] })
export class UsersModule {}

2. Thin Controller

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
  return this.usersService.findOne(id);
}

3. Service with @Injectable()

@Injectable()
export class UsersService { }

4. Constructor Injection

constructor(private readonly usersService: UsersService) {}

5. Custom Provider

{ provide: 'CONFIG', useValue: { port: 3000 } }

6. Factory Provider

{ provide: 'DB', useFactory: (cfg) => createConnection(cfg), inject: [ConfigService] }

7. Global Validation

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

8. PartialType Update DTO

export class UpdateUserDto extends PartialType(CreateUserDto) {}

9. Root Module

@Module({ imports: [UsersModule, OrdersModule] })
export class AppModule {}

10. Bootstrap

const app = await NestFactory.create(AppModule);
await app.listen(3000);

Visual

The Module Graph

┌──────────────────────────────────────────────────────────────┐
│  MODULE GRAPH                                                │
│                                                              │
│  AppModule (root)                                            │
│    │                                                         │
│    ├── imports: UsersModule                                  │
│    │     ├── controllers: UsersController                    │
│    │     ├── providers:   UsersService                       │
│    │     └── exports:     UsersService                       │
│    │                                                         │
│    └── imports: OrdersModule                                 │
│          ├── imports:     UsersModule                        │
│          ├── controllers: OrdersController                   │
│          ├── providers:   OrdersService                      │
│          │                                               │
│          │   OrdersService injects UsersService             │
│          │   from the imported UsersModule.                  │
│          │                                               │
│          └── exports:     (none)                             │
│                                                              │
│  UsersModule does not import OrdersModule.                   │
│  It cannot inject OrdersService.                             │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Dependency Injection Resolution

┌──────────────────────────────────────────────────────────────┐
│  DEPENDENCY INJECTION RESOLUTION                             │
│                                                              │
│  @Controller('users')                                        │
│  export class UsersController {                              │
│    constructor(private usersService: UsersService) {}        │
│  }                                                           │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  1. NestJS reads constructor metadata                │    │
│  │     parameter type = UsersService                    │    │
│  │                                                       │    │
│  │  2. Container looks up UsersService                  │    │
│  │                                                       │    │
│  │  3. Finds it in UsersModule.providers                │    │
│  │                                                       │    │
│  │  4. Creates instance (singleton)                     │    │
│  │                                                       │    │
│  │  5. Injects into UsersController constructor         │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  UsersService itself has dependencies:                       │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  UsersService                                        │    │
│  │    └── DatabaseService (from DatabaseModule)          │    │
│  │          └── ConfigService (from ConfigModule)        │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  The container resolves the entire graph before the          │
│  application starts.                                         │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Singleton vs Duplicate Provider

┌──────────────────────────────────────────────────────────────┐
│  SINGLETON vs DUPLICATE PROVIDER                             │
│                                                              │
│  CORRECT: Import the module                                  │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  UsersModule exports UsersService                    │    │
│  │  OrdersModule imports UsersModule                    │    │
│  │                                                       │    │
│  │  UsersService is instantiated ONCE.                  │    │
│  │  Both modules share the same instance.               │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  INCORRECT: Re-declare the provider                          │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  UsersModule providers: [UsersService]               │    │
│  │  OrdersModule providers: [UsersService]              │    │
│  │                                                       │    │
│  │  UsersService is instantiated TWICE.                 │    │
│  │  State changes in one instance are invisible         │    │
│  │  to the other.                                       │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  Rule: Declare a provider once. Import the module            │
│  that exports it.                                            │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Request Flow

┌──────────────────────────────────────────────────────────────┐
│  REQUEST FLOW                                                │
│                                                              │
│  HTTP Request: POST /users                                   │
│    │                                                         │
│    ▼                                                         │
│  Middleware ──────────────► (if configured)                  │
│    │                                                         │
│    ▼                                                         │
│  Guards ──────────────────► (if configured)                  │
│    │                                                         │
│    ▼                                                         │
│  Pipes ───────────────────► ValidationPipe                   │
│    │                         transforms @Body to DTO         │
│    │                         validates class-validator rules │
│    │                                                         │
│    ▼                                                         │
│  UsersController.create(dto)                                 │
│    │                                                         │
│    │  delegates to                                          │
│    ▼                                                         │
│  UsersService.create(dto)                                    │
│    │                                                         │
│    │  contains business logic                               │
│    │  throws NotFoundException if not found                 │
│    ▼                                                         │
│  Response ───────────────► serialized to JSON                │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
ModuleOrganizational boundary with imports, controllers, providers, exports
ControllerHTTP entry point; delegates to services
ServiceBusiness logic; marked with @Injectable()
Dependency injectionContainer resolves constructor parameters by type
Default scopeSingleton; one instance per application
Custom providersuseValue, useFactory, useClass, useExisting
ExportsModule’s public API; unexported providers are private
ValidationValidationPipe with class-validator DTOs
TestabilityContainer can be overridden with mocks

Key takeaways:

  • Modules are the organizational boundary. A module declares its controllers, providers, and exports. The exports array is the public API. Providers that are not exported are private. The imports array declares which other modules’ exports are consumed .
  • Controllers are thin. A controller extracts request data and delegates to a service. Business logic belongs in services. A controller that contains an if statement about business rules is a sign that the logic is in the wrong place .
  • Services contain the logic. A service is marked with @Injectable() and registered in a module’s providers array. The container instantiates it and injects it wherever it is needed .
  • Dependency injection is constructor injection. A class declares its dependencies as constructor parameters. The container resolves each parameter by type, creates or retrieves the instance, and injects it. The class never calls new on its dependencies .
  • Providers are singletons by default. The container creates one instance and reuses it. To share a provider, export it from its module and import that module elsewhere. Re-declaring the provider creates a second instance .
  • Custom providers handle non-class dependencies. Configuration objects, database connections, and swappable implementations are registered with useValue, useFactory, useClass, or useExisting, and injected with @Inject('TOKEN') .
  • Validation is declarative. DTOs with class-validator decorators define the validation rules. The ValidationPipe enforces them, stripping unknown properties and transforming types. Manual validation in the controller is an anti-pattern .
  • The container resolves the entire graph at startup. If a dependency cannot be resolved, the application fails to start, and the error identifies the unresolved token. This fail-fast behavior catches configuration mistakes before the application serves traffic .

Remember: NestJS’s architecture is a contract. A module declares what it owns and what it exposes. A controller declares its routes and delegates to services. A service declares its dependencies in the constructor and contains the logic. The container reads the metadata, resolves the graph, and wires everything together. The pieces are independent: the controller does not know how the service is constructed, the service does not know where its dependencies come from. The container is the only component that knows the full graph, and it enforces the boundaries that the modules declare. This indirection is the cost of the structure, and the structure is the benefit. For an application that will grow, the cost is worth paying.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!