DEV Community

Cover image for NestJS: The Modern Node.js Framework
Harshal Ranjhani for CodeParrot

Posted on • Originally published at codeparrot.ai

3 2 1 1 1

NestJS: The Modern Node.js Framework

NestJS is a framework for building efficient, scalable Node.js server-side applications. It uses modern JavaScript and TypeScript and is built with a philosophy that embraces both object-oriented programming, functional programming, and functional reactive programming.

Nest.js Landing

In this guide, we'll explore what makes NestJS special, why you might want to use it for your next project, and how to get started building applications with it.

What is NestJS?

NestJS is a framework built on top of Express (though it can optionally use Fastify) that adds a layer of abstraction. It takes inspiration from Angular in its modular architecture and dependency injection system.

Why was NestJS created?

Well, while Node.js offers a lot of freedom, it lacks a consistent architecture for organizing medium to large applications. NestJS provides that architecture, making it easier to build maintainable, testable applications.

NestJS Architecture

The Elegant Three-Tier Architecture of NestJS

One of the most powerful aspects of NestJS is its adherence to a clear three-tier architectural pattern that separates concerns and promotes clean code.

This architecture is specifically designed to solve the "spaghetti code" problem that plagues many Node.js applications as they grow in complexity.

NestJS Three-Tier Architecture

The three-tier architecture, as detailed in this excellent article, consists of:

  1. Controllers Layer - Acts as the entry point for all requests, handling HTTP routes and request/response cycles. Controllers are responsible for receiving user input and delegating to the appropriate services, but should contain minimal logic themselves.

  2. Service Layer - Contains all the business logic of your application. Services implement the core functionality, such as data transformations, calculations, and orchestrating the flow of data between controllers and repositories.

  3. Data Access Layer - Manages data persistence through repositories or data access objects. This layer abstracts away database operations, making it possible to switch database technologies without affecting the rest of the application.

This separation of concerns offers several benefits:

  • Maintainability: Each layer has a single responsibility, making the codebase easier to understand and modify
  • Testability: Services can be tested in isolation without the complexity of HTTP or database operations
  • Scalability: As the application grows, new components can be added within the appropriate layer without disrupting existing functionality

When requests flow through a NestJS application, they move from controllers to services and finally to the data access layer, with each layer focusing on its specific responsibility.

@Controller('users')
export class UsersController {
  constructor(private userService: UserService) {}

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

@Injectable()
export class UserService {
  constructor(private userRepository: UserRepository) {}

  async findAll(): Promise<User[]> {
    // Business logic here
    return this.userRepository.findAll();
  }
}

@Injectable()
export class UserRepository {
  async findAll(): Promise<User[]> {
    // Database access logic here
  }
}
Enter fullscreen mode Exit fullscreen mode

This architectural approach makes NestJS particularly well-suited for enterprise applications and teams working with complex domains.

Setting Up Your First NestJS Project

Getting started with NestJS is straightforward. First, you'll need to install the Nest CLI:

npm i -g @nestjs/cli
Enter fullscreen mode Exit fullscreen mode

Then, create a new project:

nest new project-name
Enter fullscreen mode Exit fullscreen mode

This sets up a new project with a basic structure. Let's take a look at what gets created. I created a project named nestjs-test and here's what I got when I opened it in my code editor:

nestjs-test in IDE

The CLI creates several files:

  • src/app.controller.ts: A basic controller with a single route
  • src/app.service.ts: A basic service
  • src/app.module.ts: The root module of the application
  • src/main.ts: The entry file of the application
  • test: A directory with testing utilities

Core Concepts in NestJS

Let's dive into some of the core concepts in NestJS:

Modules

Modules are a way to organize your application into cohesive blocks of functionality. Every NestJS application has at least one module, the root module. Here's an example of a simple module:

import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}
Enter fullscreen mode Exit fullscreen mode

Controllers

Controllers handle incoming requests and return responses to the client. Here's an expanded controller example:

import { Controller, Get, Post, Body } from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { Cat } from './interfaces/cat.interface';

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}

  @Post()
  async create(@Body() createCatDto: CreateCatDto) {
    this.catsService.create(createCatDto);
  }

  @Get()
  async findAll(): Promise<Cat[]> {
    return this.catsService.findAll();
  }
}
Enter fullscreen mode Exit fullscreen mode

Providers

Providers are classes annotated with @Injectable() that can be injected into constructors. Services, repositories, factories, and helpers can all be providers. Here's an example service:

import { Injectable } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';

@Injectable()
export class CatsService {
  private readonly cats: Cat[] = [];

  create(cat: Cat) {
    this.cats.push(cat);
  }

  findAll(): Cat[] {
    return this.cats;
  }
}
Enter fullscreen mode Exit fullscreen mode

Middleware

Middleware functions can perform tasks before a request is processed by a route handler. They can:

  • Execute code
  • Make changes to the request and response objects
  • End the request-response cycle
  • Call the next middleware in the stack

Here's a simple middleware example:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Request...');
    next();
  }
}
Enter fullscreen mode Exit fullscreen mode

Advanced Features

NestJS offers many advanced features that make it powerful for larger applications:

Data Validation

You can use DTOs (Data Transfer Objects) and the built-in validation pipes:

import { IsString, IsNotEmpty } from 'class-validator';

export class CreatePostDto {
  @IsNotEmpty()
  @IsString()
  title: string;

  @IsNotEmpty()
  @IsString()
  content: string;
}
Enter fullscreen mode Exit fullscreen mode

Then use it in your controller:

@Post()
@UsePipes(ValidationPipe)
create(@Body() createPostDto: CreatePostDto) {
  return this.blogService.create(createPostDto);
}
Enter fullscreen mode Exit fullscreen mode

Read more about validation in Nest.js here.

Database Integration

NestJS works well with TypeORM, Mongoose, Prisma, and other database tools. Here's an example of integrating TypeORM:

// app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BlogModule } from './blog/blog.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'postgres',
      password: 'postgres',
      database: 'blog',
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true,
    }),
    BlogModule,
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Authentication

NestJS provides robust authentication capabilities. Here's a simple example with Passport.js:

// auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';

@Injectable()
export class AuthService {
  constructor(
    private usersService: UsersService,
    private jwtService: JwtService,
  ) {}

  async validateUser(username: string, password: string): Promise<any> {
    const user = await this.usersService.findOne(username);
    if (user && user.password === password) {
      const { password, ...result } = user;
      return result;
    }
    return null;
  }

  async login(user: any) {
    const payload = { username: user.username, sub: user.userId };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Testing in NestJS

NestJS makes testing straightforward with built-in support for unit, integration, and end-to-end testing.

Unit Testing

Unit tests focus on testing individual components in isolation. NestJS provides tools that make it easy to mock dependencies and test service logic.

// blog.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { BlogService } from './blog.service';

describe('BlogService', () => {
  let service: BlogService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [BlogService],
    }).compile();

    service = module.get<BlogService>(BlogService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  it('should create a blog post', () => {
    const post = { id: 1, title: 'Test Post', content: 'This is a test post' };
    expect(service.create(post)).toEqual(post);
    expect(service.findAll()).toContain(post);
  });
});
Enter fullscreen mode Exit fullscreen mode

Testing Command-Line Applications

NestJS provides excellent support for building CLI applications with the nest-commander package. When building command-line tools, testing becomes even more important to ensure your commands work correctly.

Let's look at how to test a simple CLI command:

First, create a weather command that takes a city name and returns the weather:

// weather.command.ts
import { Command, CommandRunner, Option } from 'nest-commander';

@Command({ name: 'weather', description: 'Get weather for a city' })
export class WeatherCommand extends CommandRunner {
  constructor(private weatherService: WeatherService) {
    super();
  }

  async run(passedParams: string[], options?: Record<string, any>): Promise<void> {
    const city = passedParams[0] || options?.city || 'New York';
    const forecast = await this.weatherService.getWeather(city);

    console.log(`Weather for ${city}: ${forecast}`);
  }

  @Option({
    flags: '-c, --city [city]',
    description: 'City to check weather for',
  })
  parseCity(val: string): string {
    return val;
  }
}
Enter fullscreen mode Exit fullscreen mode

To test this command, we use the CommandTestFactory from the nest-commander-testing package, which provides a way to execute commands in a test environment:

// weather.command.spec.ts
import { Test } from '@nestjs/testing';
import { CommandTestFactory } from 'nest-commander-testing';
import { WeatherCommand } from './weather.command';
import { WeatherService } from './weather.service';

describe('WeatherCommand', () => {
  it('should return weather for specified city', async () => {
    // Mock the console.log function
    const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();

    // Mock the weather service
    const mockWeatherService = {
      getWeather: jest.fn().mockResolvedValue('Sunny, 75°F'),
    };

    // Create a test command using CommandTestFactory
    const commandInstance = await CommandTestFactory.createTestingCommand({
      imports: [], // Any modules you need to import
      providers: [
        WeatherCommand,
        { provide: WeatherService, useValue: mockWeatherService },
      ],
    }).compile();

    // Run the command with arguments
    await CommandTestFactory.run(commandInstance, ['weather', 'Miami', '--verbose']);

    // Verify the weather service was called with the right city
    expect(mockWeatherService.getWeather).toHaveBeenCalledWith('Miami');

    // Verify the console output
    expect(consoleLogSpy).toHaveBeenCalledWith('Weather for Miami: Sunny, 75°F');

    // Clean up
    consoleLogSpy.mockRestore();
  });

  it('should use default city when none provided', async () => {
    const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();

    const mockWeatherService = {
      getWeather: jest.fn().mockResolvedValue('Partly cloudy, 65°F'),
    };

    const commandInstance = await CommandTestFactory.createTestingCommand({
      providers: [
        WeatherCommand,
        { provide: WeatherService, useValue: mockWeatherService },
      ],
    }).compile();

    await CommandTestFactory.run(commandInstance, ['weather']);

    expect(mockWeatherService.getWeather).toHaveBeenCalledWith('New York');
    expect(consoleLogSpy).toHaveBeenCalledWith('Weather for New York: Partly cloudy, 65°F');

    consoleLogSpy.mockRestore();
  });
});
Enter fullscreen mode Exit fullscreen mode

Testing Interactive CLI Commands

For CLI applications that require user input through prompts, nest-commander-testing provides a way to mock user responses:

// pizza.command.ts
import { Command, CommandRunner } from 'nest-commander';
import { InquirerService } from 'nest-commander';

@Command({ name: 'pizza', description: 'Order a pizza' })
export class PizzaCommand extends CommandRunner {
  constructor(private readonly inquirerService: InquirerService) {
    super();
  }

  async run(): Promise<void> {
    const answers = await this.inquirerService.ask<{
      size: string;
      toppings: string[];
    }>('pizza-questions', undefined);

    console.log(`Ordering ${answers.size} pizza with ${answers.toppings.join(', ')}`);
    return Promise.resolve();
  }
}
Enter fullscreen mode Exit fullscreen mode

Testing this interactive command:

// pizza.command.spec.ts
import { CommandTestFactory } from 'nest-commander-testing';
import { PizzaCommand } from './pizza.command';

describe('PizzaCommand', () => {
  it('should order pizza with selected options', async () => {
    const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();

    const commandInstance = await CommandTestFactory.createTestingCommand({
      providers: [PizzaCommand],
    }).compile();

    // Set mock answers for the inquirer prompts
    CommandTestFactory.setAnswers({
      size: 'large',
      toppings: ['pepperoni', 'mushrooms']
    });

    await CommandTestFactory.run(commandInstance, ['pizza']);

    expect(consoleLogSpy).toHaveBeenCalledWith(
      'Ordering large pizza with pepperoni, mushrooms'
    );

    consoleLogSpy.mockRestore();
  });
});
Enter fullscreen mode Exit fullscreen mode

End-to-End Testing

// app.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';

describe('AppController (e2e)', () => {
  let app: INestApplication;

  beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    await app.init();
  });

  it('/blog (GET)', () => {
    return request(app.getHttpServer())
      .get('/blog')
      .expect(200)
      .expect([]);
  });
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

NestJS is a powerful framework that brings together the best of Node.js and TypeScript in an Angular-like structure. It provides a robust foundation for building server-side applications of all sizes, from small APIs to large-scale enterprise applications.

The learning curve might be a bit steeper than some other Node.js frameworks, but the benefits in terms of code organization, maintainability, and the ability to scale make it worth the investment.

Whether you're building a simple blog API or a complex microservices architecture, NestJS provides the tools and structure to make your development experience more productive and your applications more robust.

Resources for Further Learning

Happy coding with NestJS!

Top comments (1)

Collapse
 
micaelmi profile image
Micael Miranda Inácio

Very good article, covering all the most important topics about NestJS. I didn't know about the command feature, good to learn.