NestJS Core Architecture & Dependency Injection
1. Introduction
NestJS is a Node.js framework that follows a modular architecture to build scalable and maintainable backend applications.
It organizes backend code into different components such as Modules, Controllers and Providers.
One of the most important features of NestJS is Dependency Injection (DI), which allows developers to manage dependencies without manually creating objects.
In this blog, we will understand the core architectural components of NestJS and how they work together using practical examples.
2. Modules in NestJS
What are Modules?
A module is a class decorated with @Module() that groups related application components together.
Modules help organize an application into separate functional units.
For example, an Instagram application may contain:
- UsersModule
- PostsModule
- AuthModule
- CommentsModule
- LikesModule
Each module manages a specific functionality.
Example: UsersModule
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
Understanding the properties
-
controllers: Registers controllers belonging to the module. -
providers: Registers services and other injectable providers. -
imports: Imports other modules whose exported providers are needed. -
exports: Makes selected providers available to importing modules.
Root Module – AppModule
Every NestJS application has a root module called AppModule.
It connects different feature modules.
@Module({
imports: [
UsersModule,
PostsModule,
AuthModule,
],
})
export class AppModule {}
The AppModule acts as the starting point for organizing the application.
3. Controllers in NestJS
What are Controllers?
Controllers are responsible for handling incoming HTTP requests and returning responses to the client.
They define API endpoints using decorators.
Example
import { Controller, Get, Param } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(
private readonly usersService: UsersService
) {}
@Get(':id')
getUser(@Param('id') id: string) {
return this.usersService.getUserById(id);
}
}
Understanding the code
@Controller('users')
Defines the route prefix.
@get(':id')
Defines a GET endpoint with a dynamic route parameter.
@param('id')
Extracts the user ID from the URL.
usersService.getUserById(id)
Calls the service to retrieve the user.
For example:
GET /users/101
The controller receives the request and passes the ID to the service.
Common HTTP decorators
| Decorator | HTTP Method | Purpose |
|---|---|---|
| @get() | GET | Retrieve data |
| @post() | POST | Create data |
| @Put() | PUT | Replace data |
| @Patch() | PATCH | Partially update data |
| @Delete() | DELETE | Delete data |
Controllers should focus on request handling rather than containing all business logic.
4. Providers and Services
What are Providers?
Providers are classes or values that NestJS can manage and inject into other components.
Services are the most commonly used providers.
What are Services?
Services contain the business logic of an application.
For example, in Instagram, user-related operations such as retrieving user details, following users and unfollowing users can be implemented in a UsersService.
Example
import { Injectable } from '@nestjs/common';
@Injectable()
export class UsersService {
getUserById(id: string) {
return {
id: id,
username: 'swaroop',
displayName: 'Sai Swaroop',
};
}
}
Why do we use Services?
- To separate business logic from controllers.
- To improve code reusability.
- To make applications easier to maintain.
- To simplify unit testing.
Controller vs Service
| Controller | Service |
|---|---|
| Handles HTTP requests | Handles business logic |
| Defines API routes | Implements operations |
| Receives request data | Processes data |
| Calls services | May communicate with repositories or databases |
5. Decorators in NestJS
What are Decorators?
Decorators are special TypeScript syntax used to attach metadata to classes, methods, properties and parameters.
NestJS uses decorators extensively to define application structure and behavior.
A. @Module()
Used to define a module.
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
Purpose: Organizes related application components.
B. @Controller()
Used to define a controller.
@Controller('users')
export class UsersController {}
Purpose: Defines the route prefix for a controller.
C. @Injectable()
Used to mark a class as available for dependency injection.
@Injectable()
export class UsersService {}
Purpose: Allows NestJS to manage and inject the class as a provider.
Decorator comparison
| Decorator | Applied to | Purpose |
|---|---|---|
| @Module() | Class | Defines a module |
| @Controller() | Class | Defines a controller |
| @Injectable() | Class | Marks a class as injectable |
Other commonly used decorators include @Get(), @Post(), @Body(), @Param() and @Query().
6. Dependency Injection (DI)
What is Dependency Injection?
Dependency Injection is a design pattern in which a class receives the objects or services it requires from an external system instead of creating them manually.
Without Dependency Injection
class UsersService {
getUsers() {
return ['Swaroop', 'Rahul'];
}
}
class UsersController {
private usersService = new UsersService();
getUsers() {
return this.usersService.getUsers();
}
}
Here, the controller manually creates the service using new UsersService().
This creates a direct dependency between the two classes.
With Dependency Injection
@Injectable()
export class UsersService {
getUsers() {
return ['Swaroop', 'Rahul'];
}
}
Controller:
@Controller('users')
export class UsersController {
constructor(
private readonly usersService: UsersService
) {}
@Get()
getUsers() {
return this.usersService.getUsers();
}
}
Here, we don't manually create the service.
Instead, we declare it as a constructor dependency, and NestJS provides it automatically.
How does NestJS know what to inject?
We register the service in the module:
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}
NestJS uses this registration and the constructor metadata to resolve the dependency.
Advantages of Dependency Injection
- Reduces manual object creation.
- Improves code reusability.
- Makes testing easier.
- Reduces tight coupling.
- Makes application maintenance easier.
7. Dependency Injection Container
What is a DI Container?
The Dependency Injection Container is the mechanism in NestJS that manages providers, creates their instances and resolves their dependencies.
It acts as a central manager for application dependencies.
How does it work?
- NestJS reads the module metadata.
- It identifies registered providers.
- It checks the constructor dependencies of classes.
- It creates the required instances.
- It injects the dependencies into the respective classes.
- It manages their lifecycle according to their provider scope.
Example
Suppose both UsersController and PostsController require PrismaService.
NestJS can manage the PrismaService provider and inject its shared instance into both controllers.
By default, providers use singleton scope within their module context. NestJS also supports request-scoped and transient providers.
8. Inversion of Control (IoC)
What is Inversion of Control?
Inversion of Control is a software design principle in which the responsibility for creating and managing objects is transferred from application code to an external framework or container.
Without IoC
The developer manually creates and manages objects.
class UsersController {
usersService = new UsersService();
}
Here, the controller controls the creation of the service.
With IoC in NestJS
@Controller('users')
export class UsersController {
constructor(
private readonly usersService: UsersService
) {}
}
Here, NestJS manages the creation and injection of the service.
Relationship between IoC and DI
- IoC is a broader design principle.
- DI is one way to implement IoC.
- NestJS uses its DI container to implement dependency injection.
Advantages of IoC
- Reduces direct dependencies.
- Improves maintainability.
- Makes testing easier.
- Provides centralized dependency management.
9. How All Concepts Work Together
Consider an Instagram application where a user wants to retrieve their profile.
The frontend sends:
GET /users/101
The request follows this process:
- The UsersModule groups user-related functionality.
- The UsersController receives the HTTP request.
- The controller calls UsersService.
- The UsersService executes business logic.
- NestJS uses Dependency Injection to provide the required service.
- The DI Container manages provider instances.
- The service returns the result to the controller.
- The controller returns the HTTP response to the frontend.
This demonstrates how NestJS architecture and Dependency Injection work together.
10. Conclusion
NestJS provides a structured architecture that makes backend development easier and more maintainable.
Modules organize application functionality, controllers handle HTTP requests, and providers or services contain reusable business logic.
Decorators define the application's metadata, while Dependency Injection and the DI Container manage the dependencies between components.
Inversion of Control gives NestJS responsibility for managing object creation and dependency resolution.
Understanding these concepts provides a strong foundation for building scalable backend applications using NestJS.
Top comments (0)