跪拜 Guibai
← Back to the summary

NestJS Under the Hood: Modules, Decorators, and DI as First-Class Architecture

Foreword

When talking about Node.js backend development, the biggest adjustment for many people moving from lightweight frameworks like Express or Koa to NestJS is the flood of @ decorators, modular decomposition, and dependency injection.

NestJS is an enterprise-grade backend development framework for the Node runtime. It natively supports TypeScript by default and fully embraces modular design principles, making it well-suited for building medium-to-large web services, system integration services, and even microservice architectures.

This article strings together the most essential knowledge for getting started with NestJS — from its basic positioning and directory structure to its core components and underlying design patterns.


1. What Does Backend Development Actually Do?

Before discussing the framework, let's clarify the core work of backend development, which roughly falls into three categories:

  1. Providing API interfaces: The most common web development scenario — supplying HTTP interfaces for frontends and clients to perform CRUD operations on data.
  2. System integration and underlying services: Handling concurrency, interfacing with third-party services, encapsulating low-level capabilities, and performing task scheduling.
  3. Microservice architecture: Splitting business domains and building distributed microservice clusters to support complex business systems.

NestJS's design goal is to elegantly support all of the above scenarios through a standardized modular architecture.


2. Quick Installation and Project Initialization

NestJS provides an official CLI tool for quickly creating projects and generating code snippets for modules, controllers, etc.

1. Install the CLI globally

npm i -g @nestjs/cli

-g means global installation. Once installed, you can use the nest command in any directory on your computer; you only need to install it once.

2. Create a project

Run the following in the directory where you want to store your code:

nest new hello-nest

During execution, you will be prompted to choose a package manager, whether to enable observability, and a module system. For the beginner stage, it is recommended to choose:


3. Core Design: A Highly Modular Architectural Philosophy

NestJS's most defining characteristic is modularity. The entire application is assembled from individual modules, each with a single responsibility, managing its own controllers, services, and dependencies.

Basic Directory Structure

src
├── main.ts          # Application entry point, starts the app
├── app.module.ts    # Root module, the entry module for the entire application
├── app.controller.ts # Root controller
└── app.service.ts   # Root service

Modular Hierarchy

The entire App
    └── Root Module AppModule
            ├── Imports other sub-modules
            ├── Registers Controllers
            └── Registers Providers / Services

Each module is an independent unit that declares its dependencies and components via the @Module decorator. Modules can import and reuse each other.


4. The Core Trio: Module / Controller / Service

The three most common decorators in NestJS code correspond to three layers of responsibility. This clear layering is a typical feature of enterprise-grade frameworks.

1. @Module: The Module Assembler

@Module() is placed on a class, marking it as a Nest module. Its role is assembly and registration: telling the framework which controllers and services this module contains, and which other modules it needs to import.

import { Module } from '@nestjs/common';
import { AppController } from './app.controller.js';
import { AppService } from './app.service.js';

@Module({
  imports: [],          // Import other dependent modules
  controllers: [AppController], // Register the current module's controllers
  providers: [AppService],     // Register the current module's services (providers)
})
export class AppModule {}

2. @Controller: The Request Receiver

@Controller() marks a class as a controller. It is the entry point for HTTP requests, responsible for receiving requests, validating parameters, calling the business layer, and returning response results.

Simply put: the controller is responsible for "receiving requests, calling services, returning results" — it does not write complex business logic.

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service.js';

@Controller()
export class AppController {
  // Constructor injects the service
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    console.log('/ controller');
    // All business logic is delegated to the service layer
    return this.appService.getHello();
  }
}

3. @Service: The Business Logic Layer

The Service is where the actual business logic is written: data computation, database operations, and third-party calls all go here.

The controller layer should remain as thin as possible, with business logic pushed down into the Service. The benefits are reusable logic, easier testing, and clear responsibilities.


5. Underlying Principles: Syntactic Sugar and Design Patterns

Once you understand the usage, go one level deeper: behind NestJS's concise syntax lies the support of TypeScript language features and classic design patterns.

1. TS Constructor Parameter Modifiers

Many newcomers are puzzled: why does writing private readonly appService in the constructor allow access via this.appService?

This is TypeScript-specific syntactic sugar: when constructor parameters are prefixed with private / public / protected modifiers, the TS compiler automatically does two things:

  1. Automatically declares an instance property of the same name on the class.
  2. Automatically executes this.xxx = xxx assignment inside the constructor.
// Syntactic sugar form
constructor(private readonly appService: AppService) {}

Is equivalent to the full form:

private readonly appService: AppService;
constructor(appService: AppService) {
  this.appService = appService;
}

Note: Without a modifier, it is just a local parameter of the constructor and cannot be accessed via this outside the constructor.

2. The Decorator Pattern

The @Module, @Controller, and @Get scattered throughout the code are neither comments nor just for show — they are decorator syntax, rooted in the classic "Decorator Design Pattern."

The core idea of the decorator pattern: without modifying the original class's code, dynamically extend an object with extra functionality by wrapping the original object and adding logic before or after.

Decorators in NestJS are essentially functions that attach metadata labels to classes and methods. When the framework starts, it scans these labels and automatically completes route registration, module assembly, and dependency injection.

A simple way to understand it: decorators are like sticking electronic tags on classes/methods; the framework can read these tags and work according to them. Remove the decorator, and the corresponding functionality simply stops working.

3. Dependency Injection (DI)

You don't need to manually new AppService(). Just declare the type in the constructor, and Nest will automatically inject the instance for you.

This is dependency injection: object creation and destruction are managed uniformly by the framework; developers only need to declare dependencies without manual instantiation. The benefits are decoupling, easier testing, and unified management of instance lifecycles.


6. Summary

Finally, let's string together the core of getting started with NestJS in one sentence:

NestJS uses modularity as its skeleton, assembles components with @Module, receives requests with @Controller, handles business logic with @Service, and relies on TypeScript syntactic sugar and the decorator pattern to achieve elegant dependency injection, constructing an enterprise-grade backend architecture with high cohesion and low coupling.

Once you grasp these core concepts, writing interfaces, splitting modules, and connecting to databases will become much clearer. From here, you can continue deeper into middleware, interceptors, pipes, database integration, and other advanced topics.