Skip to content

NestJS Introduction and Core Concepts ​

What is NestJS? ​

NestJS is an enterprise-grade framework for building efficient, scalable Node.js server-side applications. Built with TypeScript, it combines elements of Object-Oriented Programming (OOP), Functional Programming (FP), and Functional Reactive Programming (FRP).

The Essence of NestJS ​

You can understand NestJS this way:

  • Express.js: Like giving you a pile of building materials and tools to build a house yourself
  • NestJS: Like giving you a complete architectural blueprint, construction team, and standardized building process
javascript
// Traditional Express.js Development
const express = require("express");
const app = express();

// Middleware, routes, controllers all mixed together
app.use(express.json());

app.get("/users", (req, res) => {
  // Business logic written directly in route handler
  User.find({}, (err, users) => {
    if (err) return res.status(500).json({ error: err });
    res.json(users);
  });
});

app.post("/users", (req, res) => {
  const user = new User(req.body);
  user.save((err) => {
    if (err) return res.status(400).json({ error: err });
    res.status(201).json(user);
  });
});

app.listen(3000);
typescript
// NestJS Enterprise Development
import { Controller, Get, Post, Body, ValidationPipe } from "@nestjs/common";
import { UserService } from "./user.service";
import { CreateUserDto } from "./dto/create-user.dto";

@Controller("users")
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  async findAll() {
    // Business logic delegated to service layer
    return await this.userService.findAll();
  }

  @Post()
  async create(@Body() createUserDto: CreateUserDto) {
    // Automatic validation, type checking
    return await this.userService.create(createUserDto);
  }
}

// Modular architecture
@Module({
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}

NestJS Design Philosophy ​

1. Architecture First ​

NestJS adopts an architecture-first design philosophy, enforcing developers to follow best practices:

typescript
// NestJS Layered Architecture
// Controller Layer - Handle HTTP requests and responses
@Controller("products")
export class ProductController {
  constructor(private readonly productService: ProductService) {}

  @Get()
  findAll(): Promise<Product[]> {
    return this.productService.findAll();
  }

  @Get(":id")
  findOne(@Param("id") id: string): Promise<Product> {
    return this.productService.findOne(id);
  }

  @Post()
  create(@Body() createProductDto: CreateProductDto): Promise<Product> {
    return this.productService.create(createProductDto);
  }

  @Put(":id")
  update(
    @Param("id") id: string,
    @Body() updateProductDto: UpdateProductDto
  ): Promise<Product> {
    return this.productService.update(id, updateProductDto);
  }

  @Delete(":id")
  remove(@Param("id") id: string): Promise<void> {
    return this.productService.remove(id);
  }
}

// Service Layer - Business logic processing
@Injectable()
export class ProductService {
  constructor(
    @InjectRepository(Product)
    private readonly productRepository: Repository<Product>
  ) {}

  async findAll(): Promise<Product[]> {
    return await this.productRepository.find();
  }

  async findOne(id: string): Promise<Product> {
    const product = await this.productRepository.findOne(id);
    if (!product) {
      throw new NotFoundException(`Product #${id} not found`);
    }
    return product;
  }

  async create(createProductDto: CreateProductDto): Promise<Product> {
    const product = this.productRepository.create(createProductDto);
    return await this.productRepository.save(product);
  }

  async update(
    id: string,
    updateProductDto: UpdateProductDto
  ): Promise<Product> {
    const existingProduct = await this.findOne(id);
    Object.assign(existingProduct, updateProductDto);
    return await this.productRepository.save(existingProduct);
  }

  async remove(id: string): Promise<void> {
    await this.findOne(id); // Validate existence
    await this.productRepository.delete(id);
  }
}

// Data Access Layer - Database operations
import {
  Entity,
  PrimaryGeneratedColumn,
  Column,
  CreateDateColumn,
  UpdateDateColumn,
} from "typeorm";

@Entity("products")
export class Product {
  @PrimaryGeneratedColumn("uuid")
  id: string;

  @Column()
  name: string;

  @Column("decimal", { precision: 10, scale: 2 })
  price: number;

  @Column({ default: true })
  available: boolean;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;
}

2. Dependency Injection ​

Dependency injection is a core pattern of NestJS, making code more modular and testable:

typescript
// Problem without dependency injection
class OrderController {
  private orderService: OrderService;
  private emailService: EmailService;
  private paymentService: PaymentService;

  constructor() {
    // Tight coupling: controller creates its own dependencies
    this.orderService = new OrderService();
    this.emailService = new EmailService();
    this.paymentService = new PaymentService();
  }

  async createOrder(orderData: CreateOrderDto) {
    // Hard to test: cannot easily replace dependencies
    const order = await this.orderService.create(orderData);
    await this.paymentService.processPayment(order.payment);
    await this.emailService.sendConfirmation(order.customerEmail);
    return order;
  }
}

// NestJS dependency injection solution
@Injectable()
export class OrderService {
  constructor(
    private readonly emailService: EmailService,
    private readonly paymentService: PaymentService,
    private readonly orderRepository: OrderRepository
  ) {}

  async createOrder(createOrderDto: CreateOrderDto): Promise<Order> {
    // Clear business logic, dependencies auto-injected
    const order = await this.orderRepository.save(createOrderDto);

    try {
      await this.paymentService.processPayment(order.payment);
      await this.emailService.sendConfirmation(order.customerEmail);
    } catch (error) {
      // Transaction rollback handling
      await this.orderRepository.remove(order);
      throw error;
    }

    return order;
  }
}

@Controller("orders")
export class OrderController {
  constructor(private readonly orderService: OrderService) {}

  @Post()
  async create(@Body() createOrderDto: CreateOrderDto) {
    // Controller focuses on HTTP layer handling
    return await this.orderService.createOrder(createOrderDto);
  }
}

// Module configuration: NestJS automatically manages dependencies
@Module({
  controllers: [OrderController],
  providers: [OrderService, EmailService, PaymentService, OrderRepository],
})
export class OrderModule {}

3. Decorator Pattern ​

Decorators make code declarative and easy to understand:

typescript
// HTTP method decorators
@Controller("users")
export class UserController {
  @Get()
  findAll() {
    return "Get all users";
  }

  @Get(":id")
  findOne(@Param("id") id: string) {
    return `Get user ${id}`;
  }

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return `Create user`;
  }

  @Put(":id")
  update(@Param("id") id: string, @Body() updateUserDto: UpdateUserDto) {
    return `Update user ${id}`;
  }

  @Delete(":id")
  remove(@Param("id") id: string) {
    return `Delete user ${id}`;
  }
}

// Validation decorators
export class CreateUserDto {
  @IsEmail()
  @IsNotEmpty()
  email: string;

  @IsString()
  @IsMinLength(3)
  @MaxLength(50)
  name: string;

  @IsString()
  @IsMinLength(8)
  @IsStrongPassword()
  password: string;

  @IsOptional()
  @IsDateString()
  birthDate?: string;

  @IsOptional()
  @IsEnum(Role)
  role?: Role;
}

// Custom decorators
export const CurrentUser = createParamDecorator(
  (data: unknown, ctx: ExecutionContext): User => {
    const request = ctx.switchToHttp().getRequest();
    return request.user;
  }
);

export const Roles = (...roles: Role[]) => SetMetadata("roles", roles);

export const RequireRoles = (...roles: Role[]): MethodDecorator => {
  return (target, propertyKey, descriptor) => {
    SetMetadata("roles", roles)(target, propertyKey, descriptor);
    UseGuards(RolesGuard)(target, propertyKey, descriptor);
  };
};

// Using custom decorators
@Controller("admin")
export class AdminController {
  @Get("dashboard")
  @RequireRoles(Role.ADMIN)
  getDashboard(@CurrentUser() user: User) {
    return `Admin ${user.name}'s dashboard`;
  }

  @Post("users")
  @RequireRoles(Role.ADMIN, Role.MODERATOR)
  createUser(@Body() createUserDto: CreateUserDto) {
    return "Create new user";
  }
}

4. Modular System ​

Modularity keeps large applications structurally clear:

typescript
// Core Module - Basic functionality
@Module({
  imports: [],
  controllers: [],
  providers: [
    // Global services
    {
      provide: APP_FILTER,
      useClass: AllExceptionsFilter,
    },
    {
      provide: APP_INTERCEPTOR,
      useClass: LoggingInterceptor,
    },
  ],
  exports: [],
})
export class CoreModule {}

// User Module
@Module({
  imports: [TypeOrmModule.forFeature([User]), AuthModule],
  controllers: [UserController],
  providers: [UserService, UserResolver],
  exports: [UserService],
})
export class UserModule {}

// Product Module
@Module({
  imports: [TypeOrmModule.forFeature([Product]), UploadModule],
  controllers: [ProductController],
  providers: [ProductService, ProductResolver],
  exports: [ProductService],
})
export class ProductModule {}

// Order Module
@Module({
  imports: [
    TypeOrmModule.forFeature([Order, OrderItem]),
    UserModule,
    ProductModule,
    PaymentModule,
  ],
  controllers: [OrderController],
  providers: [OrderService, OrderResolver],
  exports: [OrderService],
})
export class OrderModule {}

// Application Root Module
@Module({
  imports: [
    // Configuration module
    ConfigModule.forRoot({
      isGlobal: true,
      load: [databaseConfig, appConfig, authConfig],
    }),

    // Database module
    TypeOrmModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (config: ConfigService) => ({
        type: "postgres",
        host: config.get("DATABASE_HOST"),
        port: config.get("DATABASE_PORT"),
        username: config.get("DATABASE_USERNAME"),
        password: config.get("DATABASE_PASSWORD"),
        database: config.get("DATABASE_NAME"),
        entities: [__dirname + "/**/*.entity{.ts,.js}"],
        synchronize: process.env.NODE_ENV !== "production",
      }),
      inject: [ConfigService],
    }),

    // Feature modules
    CoreModule,
    AuthModule,
    UserModule,
    ProductModule,
    OrderModule,
    NotificationModule,

    // GraphQL module
    GraphQLModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: async (configService: ConfigService) => ({
        autoSchemaFile: true,
        sortSchema: true,
        playground: configService.get("NODE_ENV") !== "production",
        context: ({ req }) => ({ req }),
      }),
      inject: [ConfigService],
    }),
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

NestJS Core Features ​

1. Type-Safe Dependency Injection ​

NestJS provides a fully type-safe DI container:

typescript
// Service definition
interface ICacheService {
  get<T>(key: string): Promise<T | null>;
  set<T>(key: string, value: T, ttl?: number): Promise<void>;
  del(key: string): Promise<void>;
}

@Injectable()
export class RedisCacheService implements ICacheService {
  constructor(
    @InjectRedis() private readonly redis: Redis,
    private readonly configService: ConfigService
  ) {}

  async get<T>(key: string): Promise<T | null> {
    const value = await this.redis.get(key);
    return value ? JSON.parse(value) : null;
  }

  async set<T>(key: string, value: T, ttl?: number): Promise<void> {
    const serialized = JSON.stringify(value);
    const expiry = ttl || this.configService.get("CACHE_TTL", 3600);
    await this.redis.setex(key, expiry, serialized);
  }

  async del(key: string): Promise<void> {
    await this.redis.del(key);
  }
}

// Using abstract interface for dependency injection
@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User) private readonly userRepository: Repository<User>,
    @Inject("CacheService") private readonly cacheService: ICacheService
  ) {}

  async findById(id: string): Promise<User | null> {
    // 1. Try cache first
    const cached = await this.cacheService.get<User>(`user:${id}`);
    if (cached) return cached;

    // 2. Query database
    const user = await this.userRepository.findOne(id);
    if (!user) return null;

    // 3. Cache result
    await this.cacheService.set(`user:${id}`, user, 1800);
    return user;
  }

  async invalidateCache(id: string): Promise<void> {
    await this.cacheService.del(`user:${id}`);
  }
}

// Configure provider in module
@Module({
  providers: [
    {
      provide: "CacheService",
      useClass: RedisCacheService,
    },
  ],
})
export class CacheModule {}

2. Request Lifecycle Management ​

NestJS provides complete request lifecycle hooks:

typescript
// Global middleware
@Injectable()
export class RequestLoggerMiddleware implements NestMiddleware {
  private readonly logger = new Logger(RequestLoggerMiddleware.name);

  use(req: Request, res: Response, next: NextFunction) {
    const startTime = Date.now();
    const requestId = randomUUID();

    // Add request ID to request object
    req["requestId"] = requestId;

    // Log request start
    this.logger.log(
      `Request started: ${req.method} ${req.url}`,
      `RequestID: ${requestId}`
    );

    // Listen for response completion
    res.on("finish", () => {
      const duration = Date.now() - startTime;
      this.logger.log(
        `Request completed: ${req.method} ${req.url} - ${res.statusCode}`,
        `RequestID: ${requestId} | Duration: ${duration}ms`
      );
    });

    next();
  }
}

// Guards
@Injectable()
export class AuthGuard implements CanActivate {
  constructor(
    private readonly jwtService: JwtService,
    private readonly configService: ConfigService
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);

    if (!token) {
      throw new UnauthorizedException("Missing access token");
    }

    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: this.configService.get("JWT_SECRET"),
      });

      request["user"] = payload;
    } catch {
      throw new UnauthorizedException("Invalid access token");
    }

    return true;
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(" ") ?? [];
    return type === "Bearer" ? token : undefined;
  }
}

// Interceptors
@Injectable()
export class TransformInterceptor<T>
  implements NestInterceptor<T, ResponseDto<T>>
{
  intercept(
    context: ExecutionContext,
    next: CallHandler
  ): Observable<ResponseDto<T>> {
    return next.handle().pipe(
      map((data) => ({
        success: true,
        data,
        timestamp: new Date().toISOString(),
      })),
      catchError((error) => {
        throw new BadRequestException(error.message || "Request failed", error);
      })
    );
  }
}

// Exception Filters
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  private readonly logger = new Logger(AllExceptionsFilter.name);

  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    let status = HttpStatus.INTERNAL_SERVER_ERROR;
    let message = "Internal server error";

    if (exception instanceof HttpException) {
      status = exception.getStatus();
      const exceptionResponse = exception.getResponse();
      message =
        typeof exceptionResponse === "string"
          ? exceptionResponse
          : (exceptionResponse as any).message;
    }

    this.logger.error(
      `${request.method} ${request.url}`,
      exception instanceof Error ? exception.stack : exception
    );

    response.status(status).json({
      success: false,
      statusCode: status,
      message,
      timestamp: new Date().toISOString(),
      path: request.url,
    });
  }
}

// Configure execution order in module
@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: AllExceptionsFilter,
    },
    {
      provide: APP_INTERCEPTOR,
      useClass: LoggingInterceptor,
    },
    {
      provide: APP_INTERCEPTOR,
      useClass: TransformInterceptor,
    },
  ],
})
export class AppModule {}

NestJS Advantages and Application Scenarios ​

Core Advantages of NestJS ​

  1. Enterprise-grade architecture:

    • Enforce layered architecture
    • Modular design
    • Dependency injection improves testability
  2. Type safety:

    • Native TypeScript support
    • Compile-time type checking
    • Intelligent code suggestions
  3. Rich ecosystem:

    • Built-in WebSocket support
    • GraphQL out of the box
    • Microservices architecture support
  4. Developer experience:

    • Hot reload
    • Rich CLI tools
    • Complete testing support

Suitable Scenarios ​

NestJS is particularly suitable for the following types of projects:

  1. Enterprise web applications: Requiring strict architecture and maintainability
  2. Microservices architecture: Modular design naturally supports microservices
  3. GraphQL API: Built-in support, type-safe GraphQL development
  4. Real-time applications: WebSocket support and event-driven architecture
  5. Large team collaboration: Standardized code structure and development workflow

Summary ​

NestJS provides an enterprise-grade solution for Node.js application development by combining modern software architecture patterns with TypeScript's type safety. It not only improves code maintainability and testability but also makes backend development more efficient and enjoyable through rich features and excellent developer experience.

Core Value of NestJS ​

  1. Architectural consistency: Enforce best practices, reduce technical debt
  2. Type safety: Native TypeScript support, reduce runtime errors
  3. Modular design: Clear module boundaries, facilitate team collaboration
  4. Rich features: WebSocket, GraphQL, microservices out of the box
  5. Excellent developer experience: CLI tools, hot reload, testing support

Mastering NestJS not only enables you to build high-quality enterprise applications but also helps you deeply understand the essence of modern software architecture design. When building large, complex backend applications, NestJS is a very worthy choice.

Last updated: