⚡ TECH BLOG
Home
Blog
Tags
About
⚡

Powered by Next.js 15 & Modern Web Tech ⚡

Back to Home

NestJS Fundamentals: Building Scalable Node.js Applications

May 15, 2022
nestjsnodejstypescriptbackend
NestJS Fundamentals: Building Scalable Node.js Applications

NestJS Fundamentals: Building Scalable Node.js Applications

NestJS is a progressive Node.js framework for building efficient, scalable server-side applications. Built with TypeScript, it combines elements of OOP, FP, and FRP to provide a robust architecture.

Why NestJS?

  • TypeScript native - Full TypeScript support out of the box
  • Modular architecture - Organized, maintainable code structure
  • Dependency injection - Built-in IoC container
  • Express under the hood - Leverage the Express ecosystem
  • Excellent CLI - Scaffolding and code generation
  • Great documentation - Comprehensive guides and examples

Getting Started

Installation

# Install Nest CLI
npm install -g @nestjs/cli

# Create new project
nest new my-project

# Navigate to project
cd my-project

# Start development server
npm run start:dev

Project Structure

my-project/
├── src/
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test/
├── package.json
├── tsconfig.json
└── nest-cli.json

Core Concepts

Modules

Modules organize the application into logical pieces.

// src/users/users.module.ts
import { Module } from '@nestjs/common'
import { UsersController } from './users.controller'
import { UsersService } from './users.service'

@Module({
  controllers: [UsersController],
  providers: [UsersService],
  exports: [UsersService], // Make available to other modules
})
export class UsersModule {}

Controllers

Controllers handle incoming requests and return responses.

// src/users/users.controller.ts
import { Controller, Get, Post, Body, Param, Put, Delete } from '@nestjs/common'
import { UsersService } from './users.service'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  findAll() {
    return this.usersService.findAll()
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.usersService.findOne(+id)
  }

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return this.usersService.create(createUserDto)
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
    return this.usersService.update(+id, updateUserDto)
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.usersService.remove(+id)
  }
}

Providers (Services)

Providers contain business logic and can be injected as dependencies.

// src/users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common'

@Injectable()
export class UsersService {
  private users = [
    { id: 1, name: 'John Doe', email: 'john@example.com' },
    { id: 2, name: 'Jane Doe', email: 'jane@example.com' },
  ]

  findAll() {
    return this.users
  }

  findOne(id: number) {
    const user = this.users.find(u => u.id === id)
    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`)
    }
    return user
  }

  create(createUserDto: CreateUserDto) {
    const newUser = {
      id: this.users.length + 1,
      ...createUserDto,
    }
    this.users.push(newUser)
    return newUser
  }

  update(id: number, updateUserDto: UpdateUserDto) {
    const userIndex = this.users.findIndex(u => u.id === id)
    if (userIndex === -1) {
      throw new NotFoundException(`User with ID ${id} not found`)
    }
    this.users[userIndex] = { ...this.users[userIndex], ...updateUserDto }
    return this.users[userIndex]
  }

  remove(id: number) {
    const userIndex = this.users.findIndex(u => u.id === id)
    if (userIndex === -1) {
      throw new NotFoundException(`User with ID ${id} not found`)
    }
    this.users.splice(userIndex, 1)
    return { deleted: true }
  }
}

Data Transfer Objects (DTOs)

DTOs define the shape of data for validation and documentation.

// src/users/dto/create-user.dto.ts
import { IsString, IsEmail, IsNotEmpty } from 'class-validator'

export class CreateUserDto {
  @IsString()
  @IsNotEmpty()
  name: string

  @IsEmail()
  email: string
}
// src/users/dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types'
import { CreateUserDto } from './create-user.dto'

export class UpdateUserDto extends PartialType(CreateUserDto) {}

Dependency Injection

NestJS has a built-in IoC container for dependency injection.

// Custom provider
@Module({
  providers: [
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: async () => {
        const connection = await createConnection({
          type: 'mysql',
          host: 'localhost',
          port: 3306,
          username: 'root',
          password: 'password',
          database: 'test',
        })
        return connection
      },
    },
  ],
})
export class DatabaseModule {}

// Injecting custom provider
@Injectable()
export class UsersService {
  constructor(@Inject('DATABASE_CONNECTION') private connection) {}
}

Middleware

Middleware functions have access to the request and response objects.

// src/common/middleware/logger.middleware.ts
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(`[${new Date().toISOString()}] ${req.method} ${req.url}`)
    next()
  }
}

// Apply in module
@Module({
  // ...
})
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('users')
  }
}

Exception Handling

Built-in Exceptions

import { 
  BadRequestException, 
  UnauthorizedException, 
  NotFoundException, 
  ForbiddenException 
} from '@nestjs/common'

// Usage in service
throw new BadRequestException('Invalid input data')
throw new UnauthorizedException('Invalid credentials')
throw new NotFoundException('Resource not found')
throw new ForbiddenException('Access denied')

Exception Filters

// src/common/filters/http-exception.filter.ts
import { 
  ExceptionFilter, 
  Catch, 
  ArgumentsHost, 
  HttpException, 
  HttpStatus 
} from '@nestjs/common'
import { Request, Response } from 'express'

@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp()
    const response = ctx.getResponse<Response>()
    const request = ctx.getRequest<Request>()

    const status =
      exception instanceof HttpException
        ? exception.getStatus()
        : HttpStatus.INTERNAL_SERVER_ERROR

    const message =
      exception instanceof HttpException
        ? exception.getResponse()
        : 'Internal server error'

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: typeof message === 'string' ? message : (message as any).message,
    })
  }
}

// Apply globally
async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  app.useGlobalFilters(new HttpExceptionFilter())
  await app.listen(3000)
}

Pipes

Pipes transform input data or validate it.

Built-in Pipes

import { ParseIntPipe, ValidationPipe } from '@nestjs/common'

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
  return this.usersService.findOne(id)
}

// Global validation pipe
async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    transform: true,
  }))
  await app.listen(3000)
}

Custom Pipe

// src/common/pipes/parse-int.pipe.ts
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common'

@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
  transform(value: string): number {
    const val = parseInt(value, 10)
    if (isNaN(val)) {
      throw new BadRequestException('Validation failed (numeric string is expected)')
    }
    return val
  }
}

Guards

Guards determine whether a request should be handled.

// src/auth/guards/jwt-auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest()
    const token = this.extractTokenFromHeader(request)
    
    if (!token) {
      throw new UnauthorizedException()
    }
    
    try {
      const payload = await this.jwtService.verifyAsync(token)
      request.user = payload
    } catch {
      throw new UnauthorizedException()
    }
    
    return true
  }

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

// Apply to controller
@Controller('users')
@UseGuards(JwtAuthGuard)
export class UsersController {
  // ...
}

Interceptors

Interceptors bind extra logic before/after method execution.

// src/common/interceptors/logging.interceptor.ts
import { 
  Injectable, 
  NestInterceptor, 
  ExecutionContext, 
  CallHandler 
} from '@nestjs/common'
import { Observable } from 'rxjs'
import { tap } from 'rxjs/operators'

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest()
    const now = Date.now()
    
    return next
      .handle()
      .pipe(
        tap(() => console.log(
          `${request.method} ${request.url} - ${Date.now() - now}ms`
        ))
      )
  }
}

// Response transformation
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
    return next.handle().pipe(
      map(data => ({
        statusCode: context.switchToHttp().getResponse().statusCode,
        data,
        timestamp: new Date().toISOString(),
      }))
    )
  }
}

Database Integration with TypeORM

Setup

npm install @nestjs/typeorm typeorm mysql2

Configuration

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

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: 'localhost',
      port: 3306,
      username: 'root',
      password: 'password',
      database: 'test',
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

Entity

// src/users/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'

@Entity('users')
export class User {
  @PrimaryGeneratedColumn()
  id: number

  @Column()
  name: string

  @Column({ unique: true })
  email: string

  @Column({ default: true })
  isActive: boolean

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  createdAt: Date
}

Repository Pattern

// src/users/users.service.ts
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { User } from './user.entity'

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private usersRepository: Repository<User>,
  ) {}

  findAll(): Promise<User[]> {
    return this.usersRepository.find()
  }

  findOne(id: number): Promise<User> {
    return this.usersRepository.findOneBy({ id })
  }

  async create(createUserDto: CreateUserDto): Promise<User> {
    const user = this.usersRepository.create(createUserDto)
    return this.usersRepository.save(user)
  }

  async remove(id: number): Promise<void> {
    await this.usersRepository.delete(id)
  }
}

Testing

Unit Testing

// src/users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing'
import { UsersService } from './users.service'

describe('UsersService', () => {
  let service: UsersService

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

    service = module.get<UsersService>(UsersService)
  })

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

  it('should return all users', () => {
    const result = service.findAll()
    expect(result).toBeInstanceOf(Array)
  })
})

E2E Testing

// test/users.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('UsersController (e2e)', () => {
  let app: INestApplication

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

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

  it('/users (GET)', () => {
    return request(app.getHttpServer())
      .get('/users')
      .expect(200)
      .expect(res => {
        expect(Array.isArray(res.body)).toBe(true)
      })
  })
})

Conclusion

NestJS provides a robust foundation for building scalable server-side applications. Its modular architecture, dependency injection, and comprehensive tooling make it an excellent choice for enterprise-grade Node.js applications.

Key Takeaways

  • Use modules to organize your code
  • Leverage dependency injection for clean architecture
  • Implement guards for authentication and authorization
  • Use pipes for validation and transformation
  • Write comprehensive tests for reliability

Start building your next backend project with NestJS and experience the difference!

Share:

💬 Comments