DEV Community

Cover image for How To Get Started From Express To NestJS: Complete Guide
Bliss Abhademere
Bliss Abhademere

Posted on • Updated on

How To Get Started From Express To NestJS: Complete Guide

Introduction:

Backend Developers like myself in the Node.js space looking to transition from Express.js a flexible and often straight forward framework to Nest.js a more structured and organized architecture to enhance the maintainability and scalability of your application. In this guide, we'll explore the key concepts of Nest.js, providing detailed examples and code snippets along the way.

Advantages of NestJS over Express:

NestJS offers several benefits that makes it an appealing alternative choice for developers:

  • Modularity: NestJS is built with modularity in mind, allowing you to organize your code into manageable modules.

  • Dependency Injection: NestJS also comes with built-in dependency which simplifies code organization and promotes testability.

  • Decorators and Metadata: NestJS use of decorators and metadata simplifies the creation of controllers, services, and modules.

  • Typescript and Microservices: NestJS is built with Typescript and Microservices in mind, providing strong typing and enhanced developer experience in using microservices like RabbitMQ, Kafka.

Resources

NestJS Project Setup

NestJS welcome page

Installing the NestJS CLI

To get started, install the NestJS CLI globally:

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

The NestJS CLI simplies nest project creation and component generation, providing a seamless development experience.

NestJS Project Creation

Next, we create a new NestJS project named "my-nest-app":

nest new my-nest-app
Enter fullscreen mode Exit fullscreen mode

After this command is executed on your CLI you get a prompt asking your preferred package manager:

⚡  We will scaffold your app in a few seconds..

? Which package manager would you ❤️  to use? (Use arrow keys)
> npm
  yarn
  pnpm
Enter fullscreen mode Exit fullscreen mode

Now once your preference has been selected, the project structure and necessary dependencies installs. Navigate to the project directory:

cd my-nest-app
Enter fullscreen mode Exit fullscreen mode

Application Structures: NestJS vs. Express

Let's compare the directory structures of NestJS and Express: NestJS Project Structure

src/
|-- controllers/
|   |-- app.controller.ts
|-- modules/
|   |-- app.module.ts
|-- services/
|   |-- app.service.ts
|-- main.ts
Enter fullscreen mode Exit fullscreen mode

Express Project Structure

routes/
|-- appRoutes.js
|-- userRoutes.js
|-- ...
models/
|-- userModel.js
|-- ...
app.js
Enter fullscreen mode Exit fullscreen mode

In NestJS, we organize our code into modules, controllers, and services, promoting a modular and scalable architecture.

Controllers in NestJS vs. Routes in Express

Controllers flow

Introduction to Controllers

In NestJS, controllers play a crucial role in handling incoming requests and shaping the application's behavior. They're similar to Express routes but provide additional features.

Comparing Controllers to Routes

Let's consider a CRUD example to illustrate the differences between controllers in NestJS and routes in Express.

NestJS Controller

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

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

  @Get()
  findAll(): Cat[] {
    return this.catsService.findAll();
  }

  @Post()
  create(@Body() createCatDto: any): string {
    return `This action adds a new cat: ${createCatDto.name}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

The @Controller decorator specifies the base route for all the routes defined within the controller.

Express Routes

const express = require('express');
const router = express.Router();
const catsService = require('../services/cats.service');

router.get('/', (req, res) => {
  const allCats = catsService.findAll();
  res.json(allCats);
});

router.post('/', (req, res) => {
  const newCat = req.body;
  res.send(`This action adds a new cat: ${newCat.name}`);
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

In Express, routes are defined using the express.Router() and then exported.

Dependency Injection and Services

Service Injection

Understanding Dependency Injection in NestJS

NestJS embraces the concept of dependency injection (DI) for managing and injecting dependencies into components. In contrast to Express, which often relies on singleton patterns, NestJS promotes the use of injectable services.

Transitioning from Singleton Patterns to Injectable Services Let's refactor a simple Express service to a NestJS injectable service.

Express Singleton Service

class CatService {
  constructor() {
    this.cats = [];
  }

  findAll() {
    return this.cats;
  }
}

module.exports = new CatService();
Enter fullscreen mode Exit fullscreen mode

In this example, the service is a singleton instance exported for global use.

NestJS Injectable Service

import { Injectable } from '@nestjs/common';

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

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

By using the @Injectable decorator, we define a service that can be injected into controllers or other services.

ORM in NestJS

Setting Up TypeORM Entity

NestJS supports various Object-Relational Mapping (ORM) and Object-Document Mapping (ODM) libraries. Let's set up a TypeORM entity for PostgreSQL:

InstallTypeORM and PostgreSQL Package

npm install --save @nestjs/typeorm typeorm pg
Enter fullscreen mode Exit fullscreen mode

Configure the module in app.module.ts:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CatsModule } from './cats/cats.module';

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

Define a TypeORM entity in cats/cat.entity.ts:

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';

@Entity()
export class Cat {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  age: number;

  @Column()
  breed: string;
}
Enter fullscreen mode Exit fullscreen mode

This example sets up a PostgreSQL database with TypeORM and defines a Cat entity.

Data Validation and Validation Pipes

Active Validation Error Catch

Class Validators and Global Validation Pipe

NestJS simplifies data validation using class validators and global validation pipes. Let's explore how to use these features.

Using Class Validators Create a DTO (Data Transfer Object) in cats/dto/create-cat.dto.ts:

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

export class CreateCatDto {
  @IsString()
  readonly name: string;

  @IsInt()
  readonly age: number;

  @IsString()
  readonly breed: string;
}
Enter fullscreen mode Exit fullscreen mode

In the corresponding controller, use the DTO with class validation:

import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';

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

  @Post()
  @UsePipes(new ValidationPipe({ transform: true }))
  create(@Body() createCatDto: CreateCatDto): string {
    this.catsService.create(createCatDto);
    return `Cat ${createCatDto.name} created successfully`;
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, the @UsePipes(new ValidationPipe({ transform: true })) decorator applies the global validation pipe to the create route, validating incoming data against the CreateCatDto schema. The transform: true option automatically transforms incoming payload data into instances of the DTO.

Global Validation Pipe Configuration

Configure the global validation pipe in main.ts:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ transform: true }));
  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

In this configuration, useGlobalPipes applies the validation pipe globally for all routes, ensuring that data is validated and transformed consistently across the application.

Now, every incoming request to your NestJS application will undergo validation against the defined DTO schemas, providing a robust and centralized approach to data validation.

Testing in NestJS

Writing Tests for NestJS Applications

NestJS includes built-in testing utilities that make it easy to write unit and integration tests for your application. Let's explore testing approaches using Jest in NestJS compared to testing in Express.

Setting Up Jest

NestJS Jest is a popular testing framework that works seamlessly with NestJS. Install the required packages:

npm install --save-dev @nestjs/testing jest @nestjs/schematics
Enter fullscreen mode Exit fullscreen mode

Configure Jest injest.config.js:

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
};
Enter fullscreen mode Exit fullscreen mode

Writing a Basic Unit Test

Let's write a simple unit test for a service in cats/cats.service.spec.ts:

import { Test, TestingModule } from '@nestjs/testing';
import { CatsService } from './cats.service';

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

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

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

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

  it('should return an array of cats', () => {
    const cats = service.findAll();
    expect(Array.isArray(cats)).toBe(true);
  });
});
Enter fullscreen mode Exit fullscreen mode

In this example, we're testing the CatsService to ensure it returns an array of cats.

Running Tests

Execute the tests with the following command:

npm run test
Enter fullscreen mode Exit fullscreen mode

Jest will run the tests and provide feedback on their success or failure.

Conclusion

Migrating from Express to NestJS might be overwhelming at first, but the benefits of a structured and modular architecture will greatly enhance your maintainability and scalability of your application.

Here’s a link to my main blog site: Bliss Articles

Buy Me A Coffee

I appreciate you taking the time to read this😁. Please think about giving it a ❤️ if you found it useful and instructive and bookmarking✅ for later use. Please post your queries and remarks in the comments box if you have any. I'm eager to hear your thoughts. Up until then!

Top comments (2)

Collapse
 
kostyatretyak profile image
Костя Третяк

I think this picture does not correctly show the scheme of operation of DI. I'm not sure, but most likely the injector contains a DI container.

di

Collapse
 
blissfelix3 profile image
Bliss Abhademere

I’m really wary of copyright so I try to choose images that are I think are less likely to get copyrighted. This is was the best one I could get, so I fully understand. Would likely create a sample image myself.