NestJS Summed Up: The What, Why, and How
In this article, we’ll understand some of the most important NestJS concepts using simple language and practical examples instead of…
NestJS Summed Up: The What, Why, and How

In this article, we’ll understand some of the most important NestJS concepts using simple language and practical examples instead of complicated definitions.
Why NestJS:
NestJS is a framework built on top of Express (and optionally Fastify) that provides a well-organized structure for building backend applications.
When you create a NestJS application, you’re not replacing Express. NestJS actually uses Express under the hood by default.
So why would someone choose NestJS over Express?
Imagine you’re building a small todo application. Express is an excellent choice because it’s lightweight and doesn’t force you into any particular structure.
Now imagine you’re building something much bigger:
- An e-commerce platform
- A banking system
- A SaaS product
These applications usually have dozens of developers working together. They contain hundreds of APIs, authentication, payments, notifications, reports, logging, background jobs, and much more.
Without a proper structure, the project can quickly become difficult to maintain. NestJS solves this by encouraging a modular architecture. Instead of putting everything into one application, you divide it into independent modules.
App
├── Users Module
├── Orders Module
├── Products Module
├── Payments Module
└── Auth Module
Each module is responsible for one feature.
For example, the Orders module contains everything related to orders:
- Order controller
- Order service
- Order repository
- DTOs
- Validation
If another developer is working on payments, they don’t need to understand the orders module. This separation makes the project easier to navigate, test, and maintain.
Dependency Injection (DI)
The name sounds complicated, but the idea is actually very simple. Let’s start without Dependency Injection.
Suppose you’re building an email feature.
export class EmailService {
sendEmail() {
console.log('Email sent');
}
}
Now another service wants to send emails. Without Dependency Injection, you might write something like this:
export class UserService {
private emailService = new EmailService();
registerUser() {
this.emailService.sendEmail();
}
}
It works. But there are a few problems.
First, UserService is now responsible for creating EmailService.
Second, if you later decide to replace your email provider, you’ll have to modify every place where new EmailService() appears.
Third, testing becomes difficult because you can’t easily replace the real email service with a fake one.
NestJS solves this using Dependency Injection.
Instead of creating the object yourself, you simply ask NestJS to provide it.
@Injectable()
export class EmailService {
sendEmail() {}
}
Then:
@Injectable()
export class UserService {
constructor(
private readonly emailService: EmailService,
) {}
registerUser() {
this.emailService.sendEmail();
}
}
Notice something important.
There is no:
new EmailService()
NestJS creates the object and injects it automatically. This is where the IoC (Inversion of Control) Container comes into the picture. You can think of the IoC Container as a manager that keeps track of all your application’s services. When your application starts, NestJS reads every module and registers its providers.
For example:
@Module({
providers: [EmailService, UserService],
})
export class AppModule {}
Internally, NestJS understands something like this:
EmailService
│
▼
UserService
When someone asks for UserService, NestJS notices that it depends on EmailService.
So instead of making you write:
const email = new EmailService();
const user = new UserService(email);
NestJS does it automatically.
Now let’s talk about @Injectable() because it's often misunderstood. Many developers think @Injectable() registers the class. It doesn’t. This decorator simply tells NestJS:
“This class is allowed to participate in Dependency Injection.”
The actual registration happens inside the module’s providers array.
@Module({
providers: [EmailService],
})
A simple way to remember this is:
@Injectable()= "This class can be injected."providers= "NestJS should create and manage this class."
Both are necessary.
Dependency Resolution Error
One of the first errors every NestJS developer encounters looks something like this:
Nest can't resolve dependencies of UserService.
For example:
@Injectable()
export class UserService {
constructor(
private emailService: EmailService,
) {}
}
When NestJS starts, it tries to create UserService. Since UserService needs an EmailService, NestJS asks its IoC container, "Do I know how to create an EmailService?" If the answer is no, you'll see the dependency resolution error.
The most common reasons are surprisingly simple:
- you forgot to register
EmailServicein theprovidersarray - the service is declared in another module but wasn’t exported
- the module containing
EmailServicewasn't imported into the current module - there’s a circular dependency between services.
To debug this error, read the error message carefully because NestJS usually tells you exactly which constructor parameter it couldn’t resolve. Then verify that the service is decorated with @Injectable(), registered in providers, exported if another module needs it, and that the required module has been imported.
Solving Circular Dependencies
A circular dependency happens when two classes depend on each other.
For example:
@Injectable()
export class UserService {
constructor(
private readonly emailService: EmailService,
) {}
}
@Injectable()
export class EmailService {
constructor(
private readonly userService: UserService,
) {}
}
At first glance, this might not look like a problem. But imagine what NestJS has to do. To create UserService, it first needs an EmailService.
UserService
│
▼
EmailService
Now NestJS starts creating EmailService. Unfortunately, EmailService also needs UserService.
UserService
│
▼
EmailService
│
▼
UserService
The cycle continues forever. NestJS doesn’t know which object should be created first, so it throws a circular dependency error.
The same thing can happen with modules.
UsersModule
│
▼
AuthModule
│
▼
UsersModule
So how do you solve it?
The first solution — and usually the best one — is to rethink your design.
Circular dependencies often indicate that two services are taking on too many responsibilities. Suppose both UserService and EmailService need some shared functionality. Instead of making them depend on each other, extract that logic into a third service.
UserService
│
▼
NotificationService
▲
│
EmailService
Now both services depend on NotificationService, but they no longer depend on each other.
Another approach is event-driven communication.
Imagine a user registers. Instead of UserService directly calling EmailService, it simply publishes an event saying:
"user.created"
Any service interested in that event can react.
UserService
│
▼
Event Bus
│
▼
EmailService
UserService doesn't even know EmailService exists. This creates a much looser coupling between components.
If a circular dependency is truly unavoidable, NestJS provides forwardRef(). It tells NestJS to delay resolving the dependency until both providers have been registered. While forwardRef() solves the problem, it's generally considered a workaround rather than the ideal design.
Purpose of a Module in NestJS
If Dependency Injection is the heart of NestJS, modules are its building blocks. A module groups together everything related to one feature.
Think of an e-commerce application.
Instead of putting every controller and service into one giant application, you organize them into modules.
App
├── UsersModule
├── OrdersModule
├── ProductsModule
├── PaymentsModule
└── AuthModule
Each module owns its feature.
For example:
OrdersModule
├── OrdersController
├── OrdersService
├── Order Repository
└── DTOs
This organization becomes incredibly valuable as your project grows. Inside a module, you’ll commonly see four properties.
Controllers receive incoming HTTP requests and return responses.
controllers: [OrdersController]
Providers contain the business logic.
providers: [OrdersService]
Imports allow one module to use things exported by another module.
imports: [UsersModule]
Exports decide which providers can be used outside the current module.
exports: [OrdersService]
A common beginner question is:
“Why not put everything inside AppModule?”
Technically, you can.
But imagine a project with:
- 150 services
- 80 controllers
- 40 repositories
Putting everything into one module would make the application difficult to understand and maintain.
NestJS commonly uses three types of modules:
A Feature Module contains code for one business feature.
Examples include:
- UsersModule
- ProductsModule
- OrdersModule
- AuthModule
A Shared Module contains things that multiple feature modules need.
For example:
SharedModule
├── LoggerService
├── CacheService
├── Utility Helpers
└── Common Pipes
Instead of duplicating these across every feature, they are exported from the shared module and imported wherever needed.
A Global Module is different.
When a module is marked with @Global(), you only register it once, and its exported providers become available throughout the application without repeatedly importing the module.
A common example is a configuration module.
ConfigModule
│
▼
Every other module can inject ConfigService
A simple way to remember these module types is:
- Feature Module → Organizes one feature.
- Shared Module → Shares reusable components.
- Global Module → Makes app-wide services available everywhere.
Request Lifecycle
Understanding the request lifecycle helps you know exactly where each NestJS feature fits.
Every HTTP request follows roughly this sequence:
Incoming Request
│
▼
Middleware
│
▼
Guards
│
▼
Interceptors (Before)
│
▼
Pipes
│
▼
Controller
│
▼
Service
│
▼
Interceptors (After)
│
▼
Response
If an error occurs at any point, Exception Filters can handle it before the response is sent.
Let’s understand each step.
Middleware
Middleware is the first stop for every incoming request. Think of it as someone standing at the entrance of your application.
It can:
- Log requests
- Parse cookies
- Add request IDs
- Initialize request context
- Perform simple preprocessing
For example, a logging middleware may simply print:
POST /orders
before the request reaches any controller. Middleware knows nothing about which controller or route handler will eventually process the request.
Guards
After middleware comes the guard. A guard answers one question:
Should this request be allowed?
This is where authentication and authorization usually happen.
Examples include:
- JWT validation
- API key verification
- Role checks
- Permission checks
If the guard returns false or throws an exception, the request stops immediately. The controller never executes.
Interceptors (Before)
Interceptors wrap the execution of your route handler. Before the controller runs, an interceptor can:
- Start a timer
- Log execution
- Modify the request
- Begin tracing
Unlike middleware, interceptors also have access to the response after the controller finishes.
Pipes
Pipes are responsible for transforming and validating incoming data.
Suppose your API expects:
{
"email": "john@example.com"
}
A validation pipe can verify:
- Required fields exist
- Email format is correct
- Numbers are actually numbers
If validation fails, NestJS immediately returns a validation error. The controller doesn’t execute.
Controller
Controllers receive the validated request. Their responsibility is simple:
Receive the request. Call the appropriate service. Return the response. Controllers should contain very little business logic.
Service
Services contain the actual business logic.
For example:
Create Order
│
Check Inventory
│
Process Payment
│
Save Database
│
Send Confirmation Email
This is where most of your application code lives.
Interceptors (After)
Once the service finishes, control returns to the interceptor.
Now it can:
- Log execution time
- Modify the response
- Cache results
- Transform response objects
For example:
Request completed in 42 ms
Exception Filters
If anything throws an exception during the request, Exception Filters can catch it. Without a filter, users might receive inconsistent error responses. With a custom exception filter, every error can follow the same format.
For example:
{
"statusCode": 400,
"message": "Invalid email",
"timestamp": "...",
"path": "/users"
}
This creates a much cleaner API for frontend developers.
A simple way to remember the lifecycle is:
- Middleware prepares the request.
- Guards decide whether it’s allowed.
- Interceptors wrap the execution.
- Pipes validate incoming data.
- Controllers receive requests.
- Services perform business logic.
- Interceptors modify the outgoing response.
- Exception Filters handle errors.
Provider Scopes
By default, NestJS creates only one instance of each provider. This is called the Singleton scope.
Suppose you have:
@Injectable()
export class UserService {}
No matter whether your application receives 10 requests or 10,000 requests, NestJS creates only one UserService.
Request 1 ─┐
Request 2 ─┼──► UserService
Request 3 ─┘
Singletons are extremely efficient because objects are created only once. This is why almost every service in a NestJS application is a singleton.
Examples include:
- EmailService
- PaymentService
- DatabaseService
Sometimes, however, you need one instance per request.
That’s where the Request scope comes in.
Imagine you’re storing request-specific information such as:
- Current user
- Tenant ID
- Correlation ID
Each request should have its own instance.
Request A
TenantContext
tenant = Google
Request B
TenantContext
tenant = Microsoft
Request-scoped providers are created once for every incoming request. The downside is performance. If your application receives 5,000 requests per minute, NestJS creates 5,000 provider instances. That’s more memory allocation and object creation compared to a singleton.
The third option is Transient scope.
A transient provider creates a brand-new instance every time it’s injected. Suppose two different services inject a PdfBuilder.
Each service receives its own builder.
OrderService
│
PdfBuilder #1
ReportService
│
PdfBuilder #2
Transient providers are useful when an object maintains its own internal state and shouldn’t be shared.
A common interview question is:
Can a Singleton inject a Request-scoped provider?
The answer is No.
Why?
A singleton is created once when the application starts. A request-scoped provider doesn’t exist until a request arrives. When NestJS creates the singleton, it has no request-specific provider to inject.
The opposite works perfectly.
A request-scoped provider can inject singleton providers because they already exist.
Today, many applications use another approach for request-specific data. Instead of making services request-scoped, they remain singletons and use AsyncLocalStorage.
Imagine every incoming request carries its own invisible backpack.
Request A
Backpack
---------
tenantId = Google
userId = 25
Request B
Backpack
---------
tenantId = Microsoft
userId = 91
Any singleton service can retrieve the current request’s data from that backpack without passing values through every method.
Custom Providers
One of the strengths of NestJS is that Dependency Injection is highly customizable.
Suppose your application supports two payment providers.
- Stripe
- PayPal
Your checkout service shouldn’t care which provider is currently being used. Instead of injecting a concrete class, you can inject an abstraction. NestJS provides three common ways to configure this.
useClass
useClass tells NestJS:
When someone asks for this dependency, create an instance of this class.
Suppose both payment providers implement the same interface. During development you might use PayPal. In production you might use Stripe. Changing one line in your provider configuration changes the implementation used throughout the application. This makes swapping implementations extremely easy.
useValue
useValue is different.
NestJS doesn’t create anything. You simply provide an existing value.
Examples include:
- Configuration objects
- Constants
- API keys
- Mock services during testing
Imagine:
{
apiKey: "...",
region: "us-east-1"
}
NestJS simply returns that object whenever it’s requested.
useFactory
Sometimes choosing the correct implementation requires logic.
For example:
If the environment is production, Use Stripe else Use PayPal:
{
provide: PaymentGateway,
useFactory: (config: ConfigService) => {
if(config.get("PAYMENT_PROVIDER") === "stripe") {
return new StripeService(...)
}
return new PaypalService(...)
},
inject: [ConfigService]
}
A factory lets you run that decision-making logic before returning the object. Factories can even inject other services like ConfigService.
This makes them perfect for creating dynamic providers based on configuration or environment variables.
A simple way to remember these three options is:
- useClass → Create an instance of this class.
- useValue → Return this existing object.
- useFactory → Run this function and return whatever it creates.
Happy coding!
메타데이터
- post_id
- 2958a886302e
- slug
- nestjs-summed-up-the-what-why-and-how-2958a886302e
- url
- https://medium.com/@awaischaudary526/nestjs-summed-up-the-what-why-and-how-2958a886302e
- canonical_url
- https://medium.com/@awaischaudary526/nestjs-summed-up-the-what-why-and-how-2958a886302e
- author_url
- https://medium.com/@awaischaudary526
- status
- ok
- fetched_at
- 2026-07-13 06:23:13