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!