← Back to list

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…

Muhammad Awais · 2026-07-08 07:45 · 0 claps · 9.4 min read
#nestjs #nodejs #system-design-interview #software-engineering #javascript
Open on Medium ↗
Wiki topics: 🌐 · Web Development

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 EmailService in the providers array
  • the service is declared in another module but wasn’t exported
  • the module containing EmailService wasn'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