🎯 empfohlene Sammlungen
Balanced sample collections from various categories for you to explore
NestJS Beispiele
NestJS Enterprise Node.js Framework-Beispiele einschließlich Module, Controller, Services und Deployment
💻 NestJS Hello World typescript
🟢 simple
Grundkonfiguration der NestJS-Anwendung und Hello World-Implementierung
// NestJS Hello World Examples
// 1. Basic NestJS Application Structure
// src/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)
// Enable validation globally
app.useGlobalPipes(new ValidationPipe())
// Enable CORS
app.enableCors()
// Prefix all routes
app.setGlobalPrefix('api')
await app.listen(3000)
console.log('🚀 Application is running on: http://localhost:3000')
}
bootstrap()
// src/app.module.ts
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
// src/app.controller.ts
import { Controller, Get, Param, Query, Post, Body } from '@nestjs/common'
import { AppService } from './app.service'
import { CreateHelloDto } from './dto/create-hello.dto'
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello()
}
@Get('hello/:name')
getHelloName(@Param('name') name: string): string {
return this.appService.getHelloName(name)
}
@Get('greet')
greet(@Query('name') name: string): object {
return this.appService.greet(name)
}
@Post('hello')
createHello(@Body() createHelloDto: CreateHelloDto): object {
return this.appService.createHello(createHelloDto)
}
}
// src/app.service.ts
import { Injectable } from '@nestjs/common'
@Injectable()
export class AppService {
getHello(): string {
return 'Hello, World!'
}
getHelloName(name: string): string {
return `Hello, ${name}!`
}
greet(name: string): object {
return {
message: `Hello, ${name || 'Guest'}!`,
timestamp: new Date().toISOString(),
framework: 'NestJS'
}
}
createHello(data: { name: string; message: string }): object {
return {
id: Math.random(),
...data,
createdAt: new Date().toISOString()
}
}
}
// src/dto/create-hello.dto.ts
import { IsString, IsOptional } from 'class-validator'
export class CreateHelloDto {
@IsString()
name: string
@IsString()
@IsOptional()
message?: string
}
// 2. NestJS with Multiple Controllers
// src/users/users.controller.ts
import { Controller, Get, Post, Put, Delete, Param, Body, HttpStatus, HttpException } from '@nestjs/common'
import { UsersService } from './users.service'
import { CreateUserDto, UpdateUserDto } from './dto'
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
async create(@Body() createUserDto: CreateUserDto) {
try {
return await this.usersService.create(createUserDto)
} catch (error) {
throw new HttpException(error.message, HttpStatus.BAD_REQUEST)
}
}
@Get()
async findAll() {
return this.usersService.findAll()
}
@Get(':id')
async findOne(@Param('id') id: string) {
const user = await this.usersService.findOne(+id)
if (!user) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND)
}
return user
}
@Put(':id')
async update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
const user = await this.usersService.update(+id, updateUserDto)
if (!user) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND)
}
return user
}
@Delete(':id')
async remove(@Param('id') id: string) {
const result = await this.usersService.remove(+id)
if (!result) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND)
}
return { message: 'User deleted successfully' }
}
}
// src/users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common'
import { CreateUserDto, UpdateUserDto } from './dto'
export interface User {
id: number
name: string
email: string
age?: number
createdAt: Date
updatedAt: Date
}
@Injectable()
export class UsersService {
private users: User[] = []
private nextId = 1
create(createUserDto: CreateUserDto): User {
const user: User = {
id: this.nextId++,
...createUserDto,
createdAt: new Date(),
updatedAt: new Date()
}
this.users.push(user)
return user
}
findAll(): User[] {
return this.users
}
findOne(id: number): User | undefined {
return this.users.find(user => user.id === id)
}
update(id: number, updateUserDto: UpdateUserDto): User | undefined {
const userIndex = this.users.findIndex(user => user.id === id)
if (userIndex === -1) {
return undefined
}
this.users[userIndex] = {
...this.users[userIndex],
...updateUserDto,
updatedAt: new Date()
}
return this.users[userIndex]
}
remove(id: number): boolean {
const userIndex = this.users.findIndex(user => user.id === id)
if (userIndex === -1) {
return false
}
this.users.splice(userIndex, 1)
return true
}
}
// src/users/dto/index.ts
export class CreateUserDto {
name: string
email: string
age?: number
}
export class UpdateUserDto {
name?: string
email?: string
age?: number
}
// 3. NestJS with Modules and Dependencies
// src/users/users.module.ts
import { Module } from '@nestjs/common'
import { UsersController } from './users.controller'
import { UsersService } from './users.service'
import { DatabaseModule } from '../database/database.module'
@Module({
imports: [DatabaseModule],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}
// src/database/database.module.ts
import { Module, Global } from '@nestjs/common'
import { DatabaseService } from './database.service'
@Global()
@Module({
providers: [DatabaseService],
exports: [DatabaseService],
})
export class DatabaseModule {}
// src/database/database.service.ts
import { Injectable } from '@nestjs/common'
export interface DatabaseConnection {
connect(): Promise<void>
disconnect(): Promise<void>
query(sql: string, params?: any[]): Promise<any>
}
@Injectable()
export class DatabaseService {
private connection: DatabaseConnection
constructor() {
this.connection = new PostgresConnection()
}
async connect() {
await this.connection.connect()
}
async disconnect() {
await this.connection.disconnect()
}
async query(sql: string, params?: any[]) {
return this.connection.query(sql, params)
}
}
class PostgresConnection implements DatabaseConnection {
async connect() {
console.log('Connecting to PostgreSQL...')
}
async disconnect() {
console.log('Disconnecting from PostgreSQL...')
}
async query(sql: string, params?: any[]) {
console.log(`Query: ${sql}`, params)
return { rows: [] }
}
}
// 4. NestJS with Custom Decorators
// src/decorators/user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
export const User = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest()
const user = request.user
return data ? user?.[data] : user
},
)
// src/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common'
export const Roles = (...roles: string[]) => SetMetadata('roles', roles)
// Usage in controllers
// src/admin/admin.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common'
import { RolesGuard } from './guards/roles.guard'
import { Roles } from '../decorators/roles.decorator'
import { User } from '../decorators/user.decorator'
@Controller('admin')
@UseGuards(RolesGuard)
@Roles('admin')
export class AdminController {
@Get('dashboard')
getDashboard(@User('id') userId: number) {
return {
message: 'Admin dashboard',
userId,
data: 'Sensitive admin data'
}
}
@Get('users')
getUsers() {
return {
message: 'All users list',
users: []
}
}
}
// 5. NestJS with Guards and Middleware
// src/guards/auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest()
const token = request.headers.authorization?.replace('Bearer ', '')
// Simple token validation
if (!token || token !== 'valid-token') {
return false
}
// Attach user to request
request.user = {
id: 1,
name: 'John Doe',
role: 'user'
}
return true
}
}
// src/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler())
if (!requiredRoles) {
return true
}
const { user } = context.switchToHttp().getRequest()
return requiredRoles.some(role => user.role === role)
}
}
// src/middleware/logging.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common'
import { Request, Response, NextFunction } from 'express'
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const start = Date.now()
const { method, url, ip } = req
console.log(`[${new Date().toISOString()}] ${method} ${url} - ${ip}`)
res.on('finish', () => {
const duration = Date.now() - start
console.log(`[${new Date().toISOString()}] ${method} ${url} - ${res.statusCode} (${duration}ms)`)
})
next()
}
}
// Apply middleware in app module
// src/app.module.ts
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'
import { LoggingMiddleware } from './middleware/logging.middleware'
import { UsersModule } from './users/users.module'
import { AuthModule } from './auth/auth.module'
@Module({
imports: [UsersModule, AuthModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggingMiddleware)
.forRoutes('*')
}
}
// 6. NestJS with Interceptors and Filters
// src/interceptors/transform.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
export interface Response<T> {
data: T
timestamp: string
success: boolean
}
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(
map(data => ({
data,
timestamp: new Date().toISOString(),
success: true
}))
)
}
}
// src/interceptors/caching.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'
import { Observable, of } from 'rxjs'
import { tap } from 'rxjs/operators'
@Injectable()
export class CachingInterceptor implements NestInterceptor {
private cache = new Map<string, any>()
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const key = context.switchToHttp().getRequest().url
if (this.cache.has(key)) {
return of(this.cache.get(key))
}
return next.handle().pipe(
tap(data => {
this.cache.set(key, data)
})
)
}
}
// src/filters/http-exception.filter.ts
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common'
import { Request, Response } from 'express'
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const response = ctx.getResponse<Response>()
const request = ctx.getRequest<Request>()
const status = exception.getStatus()
const exceptionResponse = exception.getResponse()
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
method: request.method,
message: typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message,
error: exception.constructor.name
})
}
}
// Apply globally in main.ts
// src/main.ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { ValidationPipe, HttpExceptionFilter } from '@nestjs/common'
import { TransformInterceptor } from './interceptors/transform.interceptor'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.useGlobalPipes(new ValidationPipe())
app.useGlobalFilters(new HttpExceptionFilter())
app.useGlobalInterceptors(new TransformInterceptor())
await app.listen(3000)
}
bootstrap()
// 7. NestJS with WebSocket Support
// src/websocket/websocket.gateway.ts
import {
WebSocketGateway,
SubscribeMessage,
WebSocketServer,
OnGatewayInit,
OnGatewayConnection,
OnGatewayDisconnect,
ConnectedSocket,
MessageBody,
} from '@nestjs/websockets'
import { Server } from 'socket.io'
export interface ClientData {
id: string
name: string
room?: string
}
export interface MessageData {
text: string
sender: string
timestamp: string
room?: string
}
@WebSocketGateway({ cors: { origin: '*' } })
export class WebSocketGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer() server: Server
private clients: Map<string, ClientData> = new Map()
afterInit(server: Server) {
console.log('WebSocket Gateway initialized')
}
handleConnection(client: any) {
console.log(`Client connected: ${client.id}`)
this.clients.set(client.id, {
id: client.id,
name: `User${client.id.substr(-4)}`
})
client.emit('connected', {
message: 'Connected to chat server',
clientId: client.id
})
}
handleDisconnect(client: any) {
console.log(`Client disconnected: ${client.id}`)
const clientData = this.clients.get(client.id)
this.clients.delete(client.id)
if (clientData) {
client.broadcast.emit('userLeft', {
message: `${clientData.name} left the chat`,
user: clientData
})
}
}
@SubscribeMessage('joinRoom')
handleJoinRoom(
@MessageBody() data: { room: string },
@ConnectedSocket() client: any,
) {
const clientData = this.clients.get(client.id)
if (clientData) {
clientData.room = data.room
client.join(data.room)
client.emit('joinedRoom', {
room: data.room,
message: `Joined room: ${data.room}`
})
client.to(data.room).emit('userJoinedRoom', {
message: `${clientData.name} joined the room`,
user: clientData
})
}
}
@SubscribeMessage('sendMessage')
handleMessage(
@MessageBody() data: { text: string; room?: string },
@ConnectedSocket() client: any,
) {
const clientData = this.clients.get(client.id)
if (!clientData) return
const message: MessageData = {
text: data.text,
sender: clientData.name,
timestamp: new Date().toISOString(),
room: data.room
}
if (data.room) {
this.server.to(data.room).emit('newMessage', message)
} else {
client.broadcast.emit('newMessage', message)
}
client.emit('messageSent', message)
}
@SubscribeMessage('getUsers')
handleGetUsers(@ConnectedSocket() client: any) {
const users = Array.from(this.clients.values())
client.emit('usersList', users)
}
}
// src/websocket/websocket.module.ts
import { Module } from '@nestjs/common'
import { WebSocketGateway } from './websocket.gateway'
@Module({
providers: [WebSocketGateway],
exports: [WebSocketGateway],
})
export class WebSocketModule {}
// 8. NestJS Configuration and Environment
// src/config/configuration.ts
export default () => ({
port: parseInt(process.env.PORT, 10) || 3000,
database: {
host: process.env.DATABASE_HOST || 'localhost',
port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
username: process.env.DATABASE_USERNAME || 'postgres',
password: process.env.DATABASE_PASSWORD || 'password',
database: process.env.DATABASE_NAME || 'nestjs_db'
},
jwt: {
secret: process.env.JWT_SECRET || 'default-secret',
expiresIn: process.env.JWT_EXPIRES_IN || '1h'
},
redis: {
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT, 10) || 6379,
password: process.env.REDIS_PASSWORD || undefined
}
})
// src/config/config.module.ts
import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import configuration from './configuration'
@Module({
imports: [ConfigModule.forRoot({
load: [configuration],
isGlobal: true
})],
providers: [ConfigService],
exports: [ConfigService],
})
export class AppConfigModule {}
// 9. NestJS Testing
// src/users/users.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing'
import { UsersService } from './users.service'
import { CreateUserDto } from './dto'
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()
})
describe('create', () => {
it('should create a user successfully', () => {
const createUserDto: CreateUserDto = {
name: 'John Doe',
email: '[email protected]',
age: 30
}
const user = service.create(createUserDto)
expect(user).toHaveProperty('id')
expect(user.name).toBe(createUserDto.name)
expect(user.email).toBe(createUserDto.email)
expect(user.age).toBe(createUserDto.age)
expect(user).toHaveProperty('createdAt')
expect(user).toHaveProperty('updatedAt')
})
it('should auto-increment user ID', () => {
const user1 = service.create({ name: 'User 1', email: '[email protected]' })
const user2 = service.create({ name: 'User 2', email: '[email protected]' })
expect(user2.id).toBe(user1.id + 1)
})
})
describe('findAll', () => {
it('should return empty array when no users exist', () => {
const users = service.findAll()
expect(users).toEqual([])
})
it('should return all users', () => {
const user1 = service.create({ name: 'User 1', email: '[email protected]' })
const user2 = service.create({ name: 'User 2', email: '[email protected]' })
const users = service.findAll()
expect(users).toHaveLength(2)
expect(users).toContainEqual(user1)
expect(users).toContainEqual(user2)
})
})
})
// src/users/users.controller.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication } from '@nestjs/common'
import * as request from 'supertest'
import { AppModule } from '../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()
})
describe('/users (POST)', () => {
it('should create a new user', () => {
const createUserDto = {
name: 'John Doe',
email: '[email protected]'
}
return request(app.getHttpServer())
.post('/api/users')
.send(createUserDto)
.expect(201)
.expect((res) => {
expect(res.body.name).toBe(createUserDto.name)
expect(res.body.email).toBe(createUserDto.email)
expect(res.body).toHaveProperty('id')
expect(res.body).toHaveProperty('createdAt')
})
})
it('should return 400 for invalid data', () => {
const invalidUserDto = {
name: '',
email: 'invalid-email'
}
return request(app.getHttpServer())
.post('/api/users')
.send(invalidUserDto)
.expect(400)
})
})
describe('/users (GET)', () => {
it('should return array of users', () => {
return request(app.getHttpServer())
.get('/api/users')
.expect(200)
.expect((res) => {
expect(Array.isArray(res.body)).toBe(true)
})
})
})
})
// 10. Package.json Configuration
{
"name": "nestjs-app",
"version": "0.0.1",
"description": "NestJS application example",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/websockets": "^10.0.0",
"@nestjs/config": "^3.0.0",
"@nestjs/platform-socket.io": "^10.0.0",
"class-validator": "^0.14.0",
"class-transformer": "^0.5.1",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"socket.io": "^4.7.2"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\.spec\.ts$",
"transform": {
"^.+\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}
💻 NestJS Module und Services typescript
🟡 intermediate
Modul-Architektur und Service-Patterns in NestJS-Anwendungen
// NestJS Modules and Services Examples
// 1. Basic Module Structure
// src/core/core.module.ts
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { LoggerModule } from '@nestjs/logger'
import { DatabaseModule } from './database/database.module'
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env'
}),
LoggerModule.forRoot({
isGlobal: true
}),
DatabaseModule
],
exports: [DatabaseModule]
})
export class CoreModule {}
// src/core/database/database.module.ts
import { Module } from '@nestjs/common'
import { DatabaseService } from './database.service'
@Module({
providers: [DatabaseService],
exports: [DatabaseService]
})
export class DatabaseModule {}
// src/core/database/database.service.ts
import { Injectable, OnModuleInit } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
@Injectable()
export class DatabaseService implements OnModuleInit {
private connection: any
constructor(private configService: ConfigService) {}
async onModuleInit() {
await this.connect()
}
private async connect() {
console.log('Connecting to database...')
// Database connection logic here
}
async query(sql: string, params?: any[]) {
// Query execution logic here
return { rows: [] }
}
async close() {
// Connection cleanup logic here
}
}
// 2. Feature Module Architecture
// src/users/users.module.ts
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'
import { UsersController } from './users.controller'
import { UsersService } from './users.service'
import { User } from './entities/user.entity'
import { UserProfileModule } from './user-profile/user-profile.module'
@Module({
imports: [
TypeOrmModule.forFeature([User]),
UserProfileModule
],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService]
})
export class UsersModule {}
// src/users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { User } from './entities/user.entity'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'
import { UserProfileService } from '../user-profile/user-profile.service'
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
private userProfileService: UserProfileService
) {}
async create(createUserDto: CreateUserDto): Promise<User> {
const user = this.userRepository.create(createUserDto)
const savedUser = await this.userRepository.save(user)
// Create user profile
await this.userProfileService.create({
userId: savedUser.id,
bio: createUserDto.bio || ''
})
return savedUser
}
async findAll(): Promise<User[]> {
return this.userRepository.find({
relations: ['profile']
})
}
async findOne(id: number): Promise<User> {
const user = await this.userRepository.findOne({
where: { id },
relations: ['profile']
})
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`)
}
return user
}
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
const user = await this.findOne(id)
Object.assign(user, updateUserDto)
return this.userRepository.save(user)
}
async remove(id: number): Promise<void> {
const user = await this.findOne(id)
await this.userRepository.remove(user)
}
// Custom query methods
async findByEmail(email: string): Promise<User | null> {
return this.userRepository.findOne({
where: { email },
relations: ['profile']
})
}
async findActiveUsers(): Promise<User[]> {
return this.userRepository.find({
where: { isActive: true },
relations: ['profile']
})
}
}
// 3. Service Abstraction with Interfaces
// src/users/interfaces/user-service.interface.ts
import { User } from '../entities/user.entity'
import { CreateUserDto } from '../dto/create-user.dto'
import { UpdateUserDto } from '../dto/update-user.dto'
export interface IUsersService {
create(createUserDto: CreateUserDto): Promise<User>
findAll(): Promise<User[]>
findOne(id: number): Promise<User>
update(id: number, updateUserDto: UpdateUserDto): Promise<User>
remove(id: number): Promise<void>
findByEmail(email: string): Promise<User | null>
findActiveUsers(): Promise<User[]>
}
// src/users/users.service.ts (implementing interface)
import { Injectable, NotFoundException } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { User } from './entities/user.entity'
import { CreateUserDto, UpdateUserDto } from './dto'
import { IUsersService } from './interfaces/user-service.interface'
@Injectable()
export class UsersService implements IUsersService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>
) {}
async create(createUserDto: CreateUserDto): Promise<User> {
// Implementation
}
async findAll(): Promise<User[]> {
return this.userRepository.find()
}
async findOne(id: number): Promise<User> {
const user = await this.userRepository.findOne({ where: { id } })
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`)
}
return user
}
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
const user = await this.findOne(id)
Object.assign(user, updateUserDto)
return this.userRepository.save(user)
}
async remove(id: number): Promise<void> {
const user = await this.findOne(id)
await this.userRepository.remove(user)
}
async findByEmail(email: string): Promise<User | null> {
return this.userRepository.findOne({ where: { email } })
}
async findActiveUsers(): Promise<User[]> {
return this.userRepository.find({ where: { isActive: true } })
}
}
// 4. Provider Registration and Custom Providers
// src/cache/cache.module.ts
import { Module } from '@nestjs/common'
import { CacheService } from './cache.service'
import { REDIS_PROVIDER } from './cache.constants'
// Custom provider factory
export const redisProvider = {
provide: REDIS_PROVIDER,
useFactory: async () => {
const Redis = require('ioredis')
const redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT) || 6379,
password: process.env.REDIS_PASSWORD
})
await redis.ping()
return redis
}
}
@Module({
providers: [
CacheService,
redisProvider
],
exports: [CacheService]
})
export class CacheModule {}
// src/cache/cache.service.ts
import { Injectable, Inject } from '@nestjs/common'
import { REDIS_PROVIDER } from './cache.constants'
@Injectable()
export class CacheService {
constructor(@Inject(REDIS_PROVIDER) private readonly redis: any) {}
async get(key: string): Promise<string | null> {
return this.redis.get(key)
}
async set(key: string, value: string, ttl?: number): Promise<void> {
if (ttl) {
await this.redis.setex(key, ttl, value)
} else {
await this.redis.set(key, value)
}
}
async del(key: string): Promise<void> {
await this.redis.del(key)
}
async exists(key: string): Promise<boolean> {
const result = await this.redis.exists(key)
return result === 1
}
async clear(): Promise<void> {
await this.redis.flushall()
}
}
// 5. Async Provider Registration
// src/async-connection/async-connection.module.ts
import { Module } from '@nestjs/common'
export const ASYNC_CONNECTION_PROVIDER = 'ASYNC_CONNECTION_PROVIDER'
export const asyncConnectionProvider = {
provide: ASYNC_CONNECTION_PROVIDER,
useFactory: async (configService: ConfigService) => {
// Simulate async connection setup
await new Promise(resolve => setTimeout(resolve, 1000))
return {
connect: () => console.log('Connected asynchronously'),
disconnect: () => console.log('Disconnected asynchronously')
}
},
inject: [ConfigService]
}
@Module({
providers: [asyncConnectionProvider],
exports: [ASYNC_CONNECTION_PROVIDER]
})
export class AsyncConnectionModule {}
// 6. Circular Dependency Resolution
// src/auth/auth.service.ts
import { Injectable, forwardRef, Inject } from '@nestjs/common'
import { UsersService } from '../users/users.service'
@Injectable()
export class AuthService {
constructor(
@Inject(forwardRef(() => UsersService))
private usersService: UsersService
) {}
async validateUser(email: string, password: string) {
const user = await this.usersService.findByEmail(email)
if (!user) {
return null
}
// Password validation logic here
const isValid = await this.validatePassword(password, user.password)
return isValid ? user : null
}
private async validatePassword(password: string, hashedPassword: string) {
// Password validation logic
return password === hashedPassword // Simplified
}
async login(user: any) {
return {
access_token: 'jwt-token',
user
}
}
}
// src/users/users.service.ts (updated to avoid circular dependency)
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { User } from './entities/user.entity'
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>
) {}
async findByEmail(email: string): Promise<User | null> {
return this.userRepository.findOne({ where: { email } })
}
// Other methods...
}
// 7. Service Decorators and Scopes
// src/scoped/scoped.service.ts
import { Injectable, Scope } from '@nestjs/common'
@Injectable({ scope: Scope.REQUEST })
export class ScopedService {
private requestId: string
constructor() {
this.requestId = Math.random().toString(36).substr(2, 9)
}
getRequestId(): string {
return this.requestId
}
processData(data: any): any {
return {
requestId: this.requestId,
processed: true,
data,
timestamp: new Date().toISOString()
}
}
}
// src/singleton/singleton.service.ts
import { Injectable } from '@nestjs/common'
@Injectable()
export class SingletonService {
private static instanceCount = 0
private instanceId: number
constructor() {
SingletonService.instanceCount++
this.instanceId = SingletonService.instanceCount
}
getInstanceId(): number {
return this.instanceId
}
getTotalInstances(): number {
return SingletonService.instanceCount
}
}
// 8. Service with Repository Pattern
// src/users/repositories/user.repository.ts
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { User } from '../entities/user.entity'
export interface IUserRepository {
create(userData: Partial<User>): Promise<User>
findById(id: number): Promise<User | null>
findByEmail(email: string): Promise<User | null>
findAll(): Promise<User[]>
update(id: number, userData: Partial<User>): Promise<User>
delete(id: number): Promise<void>
findActiveUsers(): Promise<User[]>
}
@Injectable()
export class UserRepository implements IUserRepository {
constructor(
@InjectRepository(User)
private readonly repository: Repository<User>
) {}
async create(userData: Partial<User>): Promise<User> {
const user = this.repository.create(userData)
return this.repository.save(user)
}
async findById(id: number): Promise<User | null> {
return this.repository.findOne({ where: { id } })
}
async findByEmail(email: string): Promise<User | null> {
return this.repository.findOne({ where: { email } })
}
async findAll(): Promise<User[]> {
return this.repository.find()
}
async update(id: number, userData: Partial<User>): Promise<User> {
await this.repository.update(id, userData)
return this.findById(id)
}
async delete(id: number): Promise<void> {
await this.repository.delete(id)
}
async findActiveUsers(): Promise<User[]> {
return this.repository.find({ where: { isActive: true } })
}
}
// src/users/users.service.ts (using repository)
import { Injectable } from '@nestjs/common'
import { CreateUserDto, UpdateUserDto } from './dto'
import { UserRepository, IUserRepository } from './repositories/user.repository'
@Injectable()
export class UsersService {
constructor(private readonly userRepository: IUserRepository) {}
async create(createUserDto: CreateUserDto): Promise<User> {
return this.userRepository.create(createUserDto)
}
async findAll(): Promise<User[]> {
return this.userRepository.findAll()
}
async findOne(id: number): Promise<User> {
const user = await this.userRepository.findById(id)
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`)
}
return user
}
async update(id: number, updateUserDto: UpdateUserDto): Promise<User> {
return this.userRepository.update(id, updateUserDto)
}
async remove(id: number): Promise<void> {
await this.userRepository.delete(id)
}
async findByEmail(email: string): Promise<User | null> {
return this.userRepository.findByEmail(email)
}
async findActiveUsers(): Promise<User[]> {
return this.userRepository.findActiveUsers()
}
}
// 9. Service with Factory Pattern
// src/factories/notification.factory.ts
import { Injectable } from '@nestjs/common'
export interface NotificationProvider {
send(message: string, recipient: string): Promise<boolean>
}
@Injectable()
export class EmailNotificationProvider implements NotificationProvider {
async send(message: string, recipient: string): Promise<boolean> {
console.log(`Sending email to ${recipient}: ${message}`)
return true
}
}
@Injectable()
export class SMSNotificationProvider implements NotificationProvider {
async send(message: string, recipient: string): Promise<boolean> {
console.log(`Sending SMS to ${recipient}: ${message}`)
return true
}
}
@Injectable()
export class PushNotificationProvider implements NotificationProvider {
async send(message: string, recipient: string): Promise<boolean> {
console.log(`Sending push notification to ${recipient}: ${message}`)
return true
}
}
export class NotificationFactory {
static createProvider(type: 'email' | 'sms' | 'push'): NotificationProvider {
switch (type) {
case 'email':
return new EmailNotificationProvider()
case 'sms':
return new SMSNotificationProvider()
case 'push':
return new PushNotificationProvider()
default:
throw new Error(`Unknown notification provider: ${type}`)
}
}
}
// src/notification/notification.service.ts
import { Injectable } from '@nestjs/common'
import { NotificationFactory, NotificationProvider } from '../factories/notification.factory'
@Injectable()
export class NotificationService {
private providers: Map<string, NotificationProvider> = new Map()
constructor() {
// Register default providers
this.providers.set('email', NotificationFactory.createProvider('email'))
this.providers.set('sms', NotificationFactory.createProvider('sms'))
this.providers.set('push', NotificationFactory.createProvider('push'))
}
async sendNotification(
type: 'email' | 'sms' | 'push',
message: string,
recipient: string
): Promise<boolean> {
const provider = this.providers.get(type)
if (!provider) {
throw new Error(`Notification provider '${type}' not found`)
}
return provider.send(message, recipient)
}
registerProvider(type: string, provider: NotificationProvider): void {
this.providers.set(type, provider)
}
async sendMultiChannelNotification(
message: string,
recipient: string,
channels: string[] = ['email', 'sms']
): Promise<boolean[]> {
const results = await Promise.all(
channels.map(async (channel) => {
try {
return await this.sendNotification(channel as any, message, recipient)
} catch (error) {
console.error(`Failed to send ${channel} notification:`, error)
return false
}
})
)
return results
}
}
// 10. Service with Event Pattern
// src/events/events.module.ts
import { Module } from '@nestjs/common'
import { EventEmitterModule } from '@nestjs/event-emitter'
import { UserEventsService } from './services/user-events.service'
import { UserCreatedListener } from './listeners/user-created.listener'
@Module({
imports: [EventEmitterModule.forRoot()],
providers: [UserEventsService, UserCreatedListener],
exports: [UserEventsService]
})
export class EventsModule {}
// src/events/services/user-events.service.ts
import { Injectable } from '@nestjs/common'
import { EventEmitter2 } from '@nestjs/event-emitter'
export interface UserCreatedEvent {
userId: number
email: string
name: string
}
export interface UserDeletedEvent {
userId: number
deletedAt: Date
}
@Injectable()
export class UserEventsService {
constructor(private eventEmitter: EventEmitter2) {}
emitUserCreated(event: UserCreatedEvent) {
this.eventEmitter.emit('user.created', event)
}
emitUserDeleted(event: UserDeletedEvent) {
this.eventEmitter.emit('user.deleted', event)
}
emitUserUpdated(userId: number, changes: any) {
this.eventEmitter.emit('user.updated', {
userId,
changes,
updatedAt: new Date()
})
}
}
// src/events/listeners/user-created.listener.ts
import { Injectable, Logger } from '@nestjs/common'
import { OnEvent } from '@nestjs/event-emitter'
import { UserCreatedEvent } from '../services/user-events.service'
import { NotificationService } from '../notification/notification.service'
import { EmailService } from '../email/email.service'
@Injectable()
export class UserCreatedListener {
private readonly logger = new Logger(UserCreatedListener.name)
constructor(
private readonly notificationService: NotificationService,
private readonly emailService: EmailService
) {}
@OnEvent('user.created')
async handleUserCreatedEvent(event: UserCreatedEvent) {
this.logger.log(`Handling user created event for user ID: ${event.userId}`)
// Send welcome email
await this.emailService.sendWelcomeEmail(event.email, event.name)
// Send welcome notification
await this.notificationService.sendNotification(
'push',
`Welcome ${event.name}! Your account has been created successfully.`,
`user_${event.userId}`
)
// Log analytics
await this.logUserCreation(event)
}
private async logUserCreation(event: UserCreatedEvent) {
// Analytics logging logic
this.logger.log(`User analytics logged for ID: ${event.userId}`)
}
}
💻 NestJS Controller und DTOs typescript
🟡 intermediate
Controller-Patterns, DTOs, Validierung und Routing in NestJS
// NestJS Controllers and DTOs Examples
// 1. Basic Controller Structure
// src/controllers/app.controller.ts
import { Controller, Get, Post, Put, Delete, Param, Query, Body, HttpStatus, HttpCode } from '@nestjs/common'
import { AppService } from '../services/app.service'
import { CreateDto, UpdateDto, QueryDto } from '../dto'
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getRoot(): string {
return this.appService.getRoot()
}
@Get('health')
@HttpCode(HttpStatus.OK)
getHealthCheck() {
return {
status: 'OK',
timestamp: new Date().toISOString(),
uptime: process.uptime()
}
}
}
// 2. RESTful Controller with Full CRUD Operations
// src/products/products.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Patch,
Param,
Query,
Body,
Headers,
Ip,
Session,
UseGuards,
UseInterceptors,
UseFilters,
HttpStatus,
NotFoundException,
BadRequestException
} from '@nestjs/common'
import { ProductsService } from '../services/products.service'
import { CreateProductDto, UpdateProductDto, ProductQueryDto } from '../dto'
import { JwtAuthGuard } from '../guards/jwt-auth.guard'
import { LoggingInterceptor } from '../interceptors/logging.interceptor'
import { HttpExceptionFilter } from '../filters/http-exception.filter'
import { CacheInterceptor } from '@nestjs/cache-manager'
import { CacheKey } from '../decorators/cache-key.decorator'
@Controller('products')
@UseGuards(JwtAuthGuard)
@UseInterceptors(LoggingInterceptor, CacheInterceptor)
@UseFilters(HttpExceptionFilter)
export class ProductsController {
constructor(private readonly productsService: ProductsService) {}
@Post()
async create(@Body() createProductDto: CreateProductDto) {
try {
const product = await this.productsService.create(createProductDto)
return {
success: true,
data: product,
message: 'Product created successfully'
}
} catch (error) {
throw new BadRequestException(error.message)
}
}
@Get()
@CacheKey('products')
async findAll(@Query() query: ProductQueryDto) {
const { page = 1, limit = 10, search, category, sortBy = 'createdAt', sortOrder = 'desc' } = query
const result = await this.productsService.findAll({
page: +page,
limit: +limit,
search,
category,
sortBy,
sortOrder
})
return {
success: true,
data: result.products,
pagination: {
page: result.page,
limit: result.limit,
total: result.total,
totalPages: Math.ceil(result.total / limit)
}
}
}
@Get(':id')
async findOne(
@Param('id', ParseIntPipe) id: number,
@Headers('accept-language') acceptLanguage: string,
@Ip() ip: string
) {
const product = await this.productsService.findOne(id)
if (!product) {
throw new NotFoundException(`Product with ID ${id} not found`)
}
// Log access
console.log(`Product ${id} accessed from ${ip} with language: ${acceptLanguage}`)
return {
success: true,
data: product
}
}
@Put(':id')
async update(
@Param('id', ParseIntPipe) id: number,
@Body() updateProductDto: UpdateProductDto
) {
const product = await this.productsService.update(id, updateProductDto)
if (!product) {
throw new NotFoundException(`Product with ID ${id} not found`)
}
return {
success: true,
data: product,
message: 'Product updated successfully'
}
}
@Patch(':id')
async partialUpdate(
@Param('id', ParseIntPipe) id: number,
@Body() partialUpdateDto: Partial<UpdateProductDto>
) {
const product = await this.productsService.partialUpdate(id, partialUpdateDto)
if (!product) {
throw new NotFoundException(`Product with ID ${id} not found`)
}
return {
success: true,
data: product,
message: 'Product partially updated successfully'
}
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id', ParseIntPipe) id: number) {
const result = await this.productsService.remove(id)
if (!result) {
throw new NotFoundException(`Product with ID ${id} not found`)
}
}
@Post(':id/like')
async likeProduct(
@Param('id', ParseIntPipe) id: number,
@Session() session: Record<string, any>
) {
const userId = session.userId || 'anonymous'
const result = await this.productsService.likeProduct(id, userId)
return {
success: true,
data: result,
message: 'Product liked successfully'
}
}
@Get(':id/reviews')
async getProductReviews(
@Param('id', ParseIntPipe) id: number,
@Query('page', ParseIntPipe) page: number = 1,
@Query('limit', ParseIntPipe) limit: number = 10
) {
const reviews = await this.productsService.getProductReviews(id, page, limit)
return {
success: true,
data: reviews,
pagination: {
page,
limit,
total: reviews.length
}
}
}
}
// 3. Controller with Custom Decorators
// src/decorators/user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
export const User = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest()
const user = request.user
return data ? user?.[data] : user
},
)
export const IpAddress = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest()
return request.ip || request.headers['x-forwarded-for'] || request.connection.remoteAddress
},
)
// src/decorators/pagination.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
export interface Pagination {
page: number
limit: number
offset: number
}
export const Pagination = createParamDecorator(
(defaultLimit: number = 10, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest()
const page = parseInt(request.query.page) || 1
const limit = parseInt(request.query.limit) || defaultLimit
return {
page,
limit,
offset: (page - 1) * limit
}
},
)
// src/controllers/user.controller.ts
import { Controller, Get, Post, Put, Delete, Param, Body } from '@nestjs/common'
import { UserService } from '../services/user.service'
import { CreateUserDto, UpdateUserDto } from '../dto'
import { User, IpAddress, Pagination } from '../decorators'
@Controller('users')
export class UserController {
constructor(private readonly userService: UserService) {}
@Get('profile')
getUserProfile(@User() user: any) {
return {
success: true,
data: user
}
}
@Get('activity')
getUserActivity(@User('id') userId: number, @Pagination(20) pagination: Pagination) {
return this.userService.getUserActivity(userId, pagination)
}
@Post()
async createUser(
@Body() createUserDto: CreateUserDto,
@IpAddress() ip: string
) {
const user = await this.userService.create({
...createUserDto,
registrationIp: ip
})
return {
success: true,
data: user,
message: 'User created successfully'
}
}
}
// 4. DTOs with Validation
// src/dto/create-product.dto.ts
import {
IsString,
IsNumber,
IsOptional,
IsPositive,
IsEmail,
IsUrl,
IsArray,
IsEnum,
MinLength,
MaxLength,
Matches,
IsDateString
} from 'class-validator'
export enum ProductStatus {
ACTIVE = 'active',
INACTIVE = 'inactive',
DRAFT = 'draft'
}
export class CreateProductDto {
@IsString()
@MinLength(3, { message: 'Name must be at least 3 characters long' })
@MaxLength(100, { message: 'Name must not exceed 100 characters' })
name: string;
@IsString()
@MinLength(10, { message: 'Description must be at least 10 characters long' })
description: string;
@IsNumber()
@IsPositive({ message: 'Price must be a positive number' })
price: number;
@IsOptional()
@IsNumber()
@IsPositive({ message: 'Discount must be a positive number' })
discount?: number;
@IsOptional()
@IsString()
@IsEnum(ProductStatus, { message: 'Invalid status value' })
status?: ProductStatus;
@IsOptional()
@IsArray()
@IsString({ each: true, message: 'Each tag must be a string' })
tags?: string[];
@IsOptional()
@IsUrl({}, { message: 'Image URL must be a valid URL' })
imageUrl?: string;
@IsOptional()
@IsString()
category?: string;
@IsOptional()
@IsNumber()
@IsPositive({ message: 'Stock must be a positive number' })
stock?: number;
@IsOptional()
@IsString()
@Matches(/^[a-zA-Z0-9-]+$/, { message: 'SKU can only contain letters, numbers, and hyphens' })
sku?: string;
@IsOptional()
@IsDateString({}, { message: 'Launch date must be a valid ISO date string' })
launchDate?: string;
}
// src/dto/update-product.dto.ts
import { PartialType } from '@nestjs/swagger'
import { CreateProductDto } from './create-product.dto'
export class UpdateProductDto extends PartialType(CreateProductDto) {}
// src/dto/product-query.dto.ts
import {
IsOptional,
IsString,
IsNumber,
IsEnum,
IsPositive,
Min,
Max
} from 'class-validator'
import { Transform } from 'class-transformer'
import { ProductStatus } from './create-product.dto'
export class ProductQueryDto {
@IsOptional()
@Transform(({ value }) => parseInt(value))
@IsNumber()
@IsPositive()
@Min(1)
page?: number = 1;
@IsOptional()
@Transform(({ value }) => parseInt(value))
@IsNumber()
@IsPositive()
@Max(100)
limit?: number = 10;
@IsOptional()
@IsString()
search?: string;
@IsOptional()
@IsString()
category?: string;
@IsOptional()
@IsEnum(ProductStatus)
status?: ProductStatus;
@IsOptional()
@IsEnum(['createdAt', 'updatedAt', 'price', 'name'])
sortBy?: string = 'createdAt';
@IsOptional()
@IsEnum(['asc', 'desc'])
sortOrder?: 'asc' | 'desc' = 'desc';
@IsOptional()
@Transform(({ value }) => value === 'true')
includeDeleted?: boolean;
@IsOptional()
@Transform(({ value }) => parseFloat(value))
@IsNumber()
@IsPositive()
minPrice?: number;
@IsOptional()
@Transform(({ value }) => parseFloat(value))
@IsNumber()
@IsPositive()
maxPrice?: number;
}
// 5. Nested DTOs for Complex Objects
// src/dto/create-order.dto.ts
import {
IsString,
IsEmail,
IsArray,
IsNumber,
IsPositive,
ValidateNested,
ArrayNotEmpty
} from 'class-validator'
import { Type } from 'class-transformer'
export class OrderItemDto {
@IsNumber()
@IsPositive()
productId: number;
@IsNumber()
@IsPositive()
quantity: number;
@IsOptional()
@IsNumber()
@IsPositive()
unitPrice?: number;
}
export class ShippingAddressDto {
@IsString()
street: string;
@IsString()
city: string;
@IsString()
state: string;
@IsString()
@Matches(/^[0-9]{5}(-[0-9]{4})?$/, { message: 'Invalid postal code format' })
postalCode: string;
@IsString()
country: string;
@IsOptional()
@IsString()
addressLine2?: string;
}
export class CreateOrderDto {
@IsEmail({}, { message: 'Invalid email format' })
customerEmail: string;
@IsString()
customerName: string;
@IsArray()
@ArrayNotEmpty({ message: 'Order must contain at least one item' })
@ValidateNested({ each: true })
@Type(() => OrderItemDto)
items: OrderItemDto[];
@ValidateNested()
@Type(() => ShippingAddressDto)
shippingAddress: ShippingAddressDto;
@IsOptional()
@IsString()
@IsEnum(['standard', 'express', 'overnight'])
shippingMethod?: string;
@IsOptional()
@IsString()
notes?: string;
}
// 6. DTOs with Class Transformer
// src/dto/user.dto.ts
import {
IsString,
IsEmail,
IsOptional,
IsBoolean,
IsDateString,
IsEnum
} from 'class-validator'
import { Transform, Expose } from 'class-transformer'
import { UserRole } from '../enums/user-role.enum'
export class CreateUserDto {
@IsString()
@Transform(({ value }) => value?.trim())
firstName: string;
@IsString()
@Transform(({ value }) => value?.trim())
lastName: string;
@IsEmail({}, { message: 'Invalid email format' })
@Transform(({ value }) => value?.toLowerCase().trim())
email: string;
@IsString()
@MinLength(8, { message: 'Password must be at least 8 characters long' })
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/, {
message: 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character'
})
password: string;
@IsOptional()
@IsDateString()
birthDate?: string;
@IsOptional()
@IsEnum(UserRole)
role?: UserRole = UserRole.USER;
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value === 'true')
isActive?: boolean = true;
@IsOptional()
@IsString()
@Transform(({ value }) => value?.trim())
phone?: string;
@IsOptional()
@Transform(({ value }) => value?.split(',').map((tag: string) => tag.trim()).filter(Boolean))
tags?: string[];
}
export class UserResponseDto {
@Expose()
id: number;
@Expose()
email: string;
@Expose()
firstName: string;
@Expose()
lastName: string;
@Expose()
fullName: string;
@Expose()
role: UserRole;
@Expose()
isActive: boolean;
@Expose()
birthDate: string;
@Expose()
phone: string;
@Expose()
tags: string[];
@Expose()
createdAt: string;
@Expose()
updatedAt: string;
@Expose()
lastLoginAt?: string;
}
// src/dto/user-search.dto.ts
import { IsOptional, IsString, IsBoolean, IsEnum } from 'class-validator'
import { Transform } from 'class-transformer'
export enum SortField {
NAME = 'name',
EMAIL = 'email',
CREATED_AT = 'createdAt',
LAST_LOGIN = 'lastLoginAt'
}
export enum SortOrder {
ASC = 'asc',
DESC = 'desc'
}
export class UserSearchDto {
@IsOptional()
@IsString()
@Transform(({ value }) => value?.trim())
search?: string;
@IsOptional()
@IsString()
@Transform(({ value }) => value?.trim())
email?: string;
@IsOptional()
@IsString()
@Transform(({ value }) => value?.trim())
name?: string;
@IsOptional()
@IsBoolean()
@Transform(({ value }) => value === 'true')
isActive?: boolean;
@IsOptional()
@IsEnum(SortField)
sortBy?: SortField = SortField.CREATED_AT;
@IsOptional()
@IsEnum(SortOrder)
sortOrder?: SortOrder = SortOrder.DESC;
@IsOptional()
@Transform(({ value }) => parseInt(value))
@IsNumber()
page?: number = 1;
@IsOptional()
@Transform(({ value }) => parseInt(value))
@IsNumber()
limit?: number = 10;
@IsOptional()
@Transform(({ value }) => value?.split(',').map((tag: string) => tag.trim()))
tags?: string[];
}
// 7. Controller with Response Transformation
// src/controllers/api/v1/users.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Param,
Body,
HttpCode,
HttpStatus,
UseGuards,
UseInterceptors
} from '@nestjs/common'
import { UserService } from '../../../services/user.service'
import { CreateUserDto, UpdateUserDto, UserResponseDto, UserSearchDto } from '../../../dto'
import { JwtAuthGuard } from '../../../guards/jwt-auth.guard'
import { TransformInterceptor } from '../../../interceptors/transform.interceptor'
import { Serialize } from '../../../decorators/serialize.decorator'
@Controller('api/v1/users')
@UseGuards(JwtAuthGuard)
@UseInterceptors(TransformInterceptor)
export class ApiV1UsersController {
constructor(private readonly userService: UserService) {}
@Post()
@Serialize(UserResponseDto)
async create(@Body() createUserDto: CreateUserDto) {
const user = await this.userService.create(createUserDto)
return {
success: true,
data: user,
message: 'User created successfully'
}
}
@Get()
@Serialize(UserResponseDto)
async findAll(@Query() query: UserSearchDto) {
const { users, pagination } = await this.userService.findAll(query)
return {
success: true,
data: users,
pagination
}
}
@Get(':id')
@Serialize(UserResponseDto)
async findOne(@Param('id') id: string) {
const user = await this.userService.findOne(+id)
return {
success: true,
data: user
}
}
@Put(':id')
@Serialize(UserResponseDto)
async update(
@Param('id') id: string,
@Body() updateUserDto: UpdateUserDto
) {
const user = await this.userService.update(+id, updateUserDto)
return {
success: true,
data: user,
message: 'User updated successfully'
}
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id') id: string) {
await this.userService.remove(+id)
}
}
// 8. Controller with File Upload
// src/controllers/upload.controller.ts
import {
Controller,
Post,
UseInterceptors,
UploadedFile,
UploadedFiles,
Body,
HttpCode,
HttpStatus,
ParseFilePipe,
MaxFileSizeValidator,
FileTypeValidator,
ParseFilePipeBuilder
} from '@nestjs/common'
import { FileInterceptor, FilesInterceptor } from '@nestjs/platform-express'
import { UploadService } from '../services/upload.service'
import { CreateUploadDto } from '../dto/upload.dto'
@Controller('upload')
export class UploadController {
constructor(private readonly uploadService: UploadService) {}
@Post('single')
@UseInterceptors(FileInterceptor('file'))
async uploadSingle(
@UploadedFile(
new ParseFilePipe({
validators: [
new MaxFileSizeValidator({ maxSize: 5 * 1024 * 1024 }), // 5MB
new FileTypeValidator({ fileType: /(jpeg|jpg|png|gif)$/i })
]
})
)
file: Express.Multer.File,
@Body() uploadDto: CreateUploadDto
) {
const result = await this.uploadService.uploadFile(file, uploadDto)
return {
success: true,
data: result,
message: 'File uploaded successfully'
}
}
@Post('multiple')
@UseInterceptors(FilesInterceptor('files', 10))
async uploadMultiple(
@UploadedFiles(
new ParseFilePipeBuilder()
.addFileTypeValidator({
fileType: /(jpeg|jpg|png|gif|pdf|doc|docx)$/i,
})
.addMaxSizeValidator({
maxSize: 10 * 1024 * 1024, // 10MB per file
maxFileSize: 50 * 1024 * 1024, // 50MB total
})
.build({
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY
})
)
files: Express.Multer.File[],
@Body() uploadDto: CreateUploadDto
) {
const results = await this.uploadService.uploadMultipleFiles(files, uploadDto)
return {
success: true,
data: results,
message: `${files.length} files uploaded successfully`
}
}
@Post('image')
@UseInterceptors(FileInterceptor('image'))
async uploadImage(
@UploadedFile(
new ParseFilePipe({
validators: [
new MaxFileSizeValidator({ maxSize: 2 * 1024 * 1024 }), // 2MB
new FileTypeValidator({ fileType: /(jpeg|jpg|png|webp)$/i })
]
})
)
image: Express.Multer.File
) {
const result = await this.uploadService.processImage(image)
return {
success: true,
data: result,
message: 'Image uploaded and processed successfully'
}
}
}
// 9. Controller with Swagger Documentation
// src/controllers/products.controller.ts
import {
Controller,
Get,
Post,
Put,
Delete,
Param,
Body,
Query,
HttpStatus
} from '@nestjs/common'
import {
ApiTags,
ApiOperation,
ApiResponse,
ApiParam,
ApiQuery,
ApiBody
} from '@nestjs/swagger'
import { ProductsService } from '../services/products.service'
import { CreateProductDto, UpdateProductDto, ProductQueryDto } from '../dto'
import { Product } from '../entities/product.entity'
@ApiTags('products')
@Controller('products')
export class ProductsController {
constructor(private readonly productsService: ProductsService) {}
@Post()
@ApiOperation({ summary: 'Create a new product' })
@ApiResponse({ status: 201, description: 'Product created successfully', type: Product })
@ApiResponse({ status: 400, description: 'Bad request - Invalid input data' })
@ApiBody({ type: CreateProductDto })
async create(@Body() createProductDto: CreateProductDto): Promise<Product> {
return this.productsService.create(createProductDto)
}
@Get()
@ApiOperation({ summary: 'Get all products with pagination and filtering' })
@ApiResponse({ status: 200, description: 'List of products', type: [Product] })
@ApiQuery({ name: 'page', required: false, type: Number, description: 'Page number' })
@ApiQuery({ name: 'limit', required: false, type: Number, description: 'Number of items per page' })
@ApiQuery({ name: 'search', required: false, type: String, description: 'Search term' })
@ApiQuery({ name: 'category', required: false, type: String, description: 'Product category' })
async findAll(@Query() query: ProductQueryDto) {
return this.productsService.findAll(query)
}
@Get(':id')
@ApiOperation({ summary: 'Get a product by ID' })
@ApiResponse({ status: 200, description: 'Product found', type: Product })
@ApiResponse({ status: 404, description: 'Product not found' })
@ApiParam({ name: 'id', description: 'Product ID' })
async findOne(@Param('id') id: string): Promise<Product> {
return this.productsService.findOne(+id)
}
@Put(':id')
@ApiOperation({ summary: 'Update a product by ID' })
@ApiResponse({ status: 200, description: 'Product updated successfully', type: Product })
@ApiResponse({ status: 404, description: 'Product not found' })
@ApiResponse({ status: 400, description: 'Bad request - Invalid input data' })
@ApiParam({ name: 'id', description: 'Product ID' })
@ApiBody({ type: UpdateProductDto })
async update(
@Param('id') id: string,
@Body() updateProductDto: UpdateProductDto
): Promise<Product> {
return this.productsService.update(+id, updateProductDto)
}
@Delete(':id')
@ApiOperation({ summary: 'Delete a product by ID' })
@ApiResponse({ status: 204, description: 'Product deleted successfully' })
@ApiResponse({ status: 404, description: 'Product not found' })
@ApiParam({ name: 'id', description: 'Product ID' })
@HttpCode(HttpStatus.NO_CONTENT)
async remove(@Param('id') id: string): Promise<void> {
return this.productsService.remove(+id)
}
}
💻 NestJS Authentifizierung und Tests typescript
🔴 complex
Authentifizierungs-Strategien, Guards und umfassende Test-Patterns
// NestJS Authentication and Testing Examples
// 1. JWT Authentication Module
// src/auth/auth.module.ts
import { Module } from '@nestjs/common'
import { JwtModule } from '@nestjs/jwt'
import { PassportModule } from '@nestjs/passport'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { AuthService } from './auth.service'
import { AuthController } from './auth.controller'
import { JwtStrategy } from './strategies/jwt.strategy'
import { LocalStrategy } from './strategies/local.strategy'
import { UsersModule } from '../users/users.module'
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: configService.get<string>('JWT_EXPIRES_IN', '1h')
}
}),
inject: [ConfigService]
})
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy, LocalStrategy],
exports: [AuthService]
})
export class AuthModule {}
// src/auth/auth.service.ts
import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { UsersService } from '../users/users.service'
import { CreateUserDto, LoginDto } from '../dto'
import * as bcrypt from 'bcrypt'
import { User } from '../entities/user.entity'
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService
) {}
async validateUser(email: string, password: string): Promise<User | null> {
const user = await this.usersService.findByEmail(email)
if (!user) {
throw new UnauthorizedException('Invalid credentials')
}
const isPasswordValid = await bcrypt.compare(password, user.password)
if (!isPasswordValid) {
throw new UnauthorizedException('Invalid credentials')
}
return user
}
async login(loginDto: LoginDto) {
const { email, password } = loginDto
const user = await this.validateUser(email, password)
if (!user.isActive) {
throw new UnauthorizedException('Account is deactivated')
}
const payload = {
sub: user.id,
email: user.email,
role: user.role,
name: user.fullName
}
const access_token = this.jwtService.sign(payload)
// Update last login
await this.usersService.updateLastLogin(user.id)
return {
access_token,
user: {
id: user.id,
email: user.email,
fullName: user.fullName,
role: user.role
}
}
}
async register(createUserDto: CreateUserDto) {
// Check if user already exists
const existingUser = await this.usersService.findByEmail(createUserDto.email)
if (existingUser) {
throw new BadRequestException('Email already registered')
}
// Hash password
const salt = await bcrypt.genSalt(10)
const hashedPassword = await bcrypt.hash(createUserDto.password, salt)
// Create user
const user = await this.usersService.create({
...createUserDto,
password: hashedPassword
})
// Generate JWT token
const payload = {
sub: user.id,
email: user.email,
role: user.role,
name: user.fullName
}
const access_token = this.jwtService.sign(payload)
return {
access_token,
user: {
id: user.id,
email: user.email,
fullName: user.fullName,
role: user.role
}
}
}
async refreshToken(user: any) {
const payload = {
sub: user.id,
email: user.email,
role: user.role,
name: user.name
}
const access_token = this.jwtService.sign(payload)
return {
access_token
}
}
async changePassword(userId: number, currentPassword: string, newPassword: string) {
const user = await this.usersService.findOne(userId)
if (!user) {
throw new UnauthorizedException('User not found')
}
const isCurrentPasswordValid = await bcrypt.compare(currentPassword, user.password)
if (!isCurrentPasswordValid) {
throw new UnauthorizedException('Current password is incorrect')
}
const salt = await bcrypt.genSalt(10)
const hashedNewPassword = await bcrypt.hash(newPassword, salt)
await this.usersService.update(userId, { password: hashedNewPassword })
return { message: 'Password changed successfully' }
}
}
// src/auth/strategies/jwt.strategy.ts
import { Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { ExtractJwt, Strategy } from 'passport-jwt'
import { ConfigService } from '@nestjs/config'
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
})
}
async validate(payload: any) {
return {
id: payload.sub,
email: payload.email,
role: payload.role,
name: payload.name
}
}
}
// src/auth/strategies/local.strategy.ts
import { Injectable } from '@nestjs/common'
import { PassportStrategy } from '@nestjs/passport'
import { Strategy } from 'passport-local'
import { AuthService } from '../auth.service'
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({
usernameField: 'email'
})
}
async validate(email: string, password: string): Promise<any> {
return await this.authService.validateUser(email, password)
}
}
// 2. Authentication Guards
// src/guards/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
// src/guards/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { ROLES_KEY } from '../decorators/roles.decorator'
import { UserRole } from '../enums/user-role.enum'
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<UserRole[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
])
if (!requiredRoles) {
return true
}
const { user } = context.switchToHttp().getRequest()
return requiredRoles.some((role) => user.role === role)
}
}
// src/guards/api-key.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
@Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(private configService: ConfigService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest()
const apiKey = request.headers['x-api-key'] as string
const validApiKeys = this.configService.get<string>('API_KEYS')?.split(',') || []
return validApiKeys.includes(apiKey)
}
}
// src/guards/throttler.guard.ts
import { Injectable, CanActivate, ExecutionContext, HttpException, HttpStatus } from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { Request } from 'express'
interface RateLimit {
windowMs: number
max: number
}
@Injectable()
export class ThrottlerGuard implements CanActivate {
private requests = new Map<string, { count: number; resetTime: number }>()
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest<Request>()
const rateLimit = this.reflector.get<RateLimit>('throttle', context.getHandler()) || {
windowMs: 60000, // 1 minute
max: 100 // 100 requests per minute
}
const key = this.getRequestKey(request)
const now = Date.now()
const record = this.requests.get(key)
if (!record || now > record.resetTime) {
this.requests.set(key, { count: 1, resetTime: now + rateLimit.windowMs })
return true
}
if (record.count >= rateLimit.max) {
throw new HttpException('Too Many Requests', HttpStatus.TOO_MANY_REQUESTS)
}
record.count++
return true
}
private getRequestKey(request: Request): string {
const ip = request.ip || request.headers['x-forwarded-for'] || request.connection.remoteAddress
return `${ip}:${request.path}`
}
}
// 3. Custom Decorators
// src/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common'
import { UserRole } from '../enums/user-role.enum'
export const ROLES_KEY = 'roles'
export const Roles = (...roles: UserRole[]) => SetMetadata(ROLES_KEY, roles)
// src/decorators/throttle.decorator.ts
import { SetMetadata } from '@nestjs/common'
export const THROTTLE_KEY = 'throttle'
export const Throttle = (windowMs: number, max: number) =>
SetMetadata(THROTTLE_KEY, { windowMs, max })
// src/decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common'
export const IS_PUBLIC_KEY = 'isPublic'
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true)
// src/decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest()
return request.user
},
)
// 4. Authentication Controller
// src/auth/auth.controller.ts
import {
Controller,
Post,
UseGuards,
Request,
Body,
HttpCode,
HttpStatus,
Get
} from '@nestjs/common'
import { AuthService } from './auth.service'
import { LocalAuthGuard } from './guards/local-auth.guard'
import { JwtAuthGuard } from './guards/jwt-auth.guard'
import { CreateUserDto, LoginDto } from '../dto'
import { CurrentUser } from '../decorators/current-user.decorator'
import { Throttle } from '../decorators/throttle.decorator'
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post('login')
@UseGuards(LocalAuthGuard)
@Throttle(60000, 5) // 5 requests per minute
@HttpCode(HttpStatus.OK)
async login(@Request() req, @Body() loginDto: LoginDto) {
return this.authService.login(loginDto)
}
@Post('register')
@Throttle(300000, 3) // 3 requests per 5 minutes
async register(@Body() createUserDto: CreateUserDto) {
return this.authService.register(createUserDto)
}
@Post('refresh')
@UseGuards(JwtAuthGuard)
async refresh(@CurrentUser() user: any) {
return this.authService.refreshToken(user)
}
@Post('change-password')
@UseGuards(JwtAuthGuard)
@Throttle(300000, 3) // 3 requests per 5 minutes
async changePassword(
@CurrentUser() user: any,
@Body() body: { currentPassword: string; newPassword: string }
) {
return this.authService.changePassword(user.id, body.currentPassword, body.newPassword)
}
@Get('profile')
@UseGuards(JwtAuthGuard)
async getProfile(@CurrentUser() user: any) {
return {
success: true,
data: user
}
}
}
// 5. Unit Testing
// src/auth/auth.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing'
import { AuthService } from './auth.service'
import { UsersService } from '../users/users.service'
import { JwtService } from '@nestjs/jwt'
import { UnauthorizedException, BadRequestException } from '@nestjs/common'
import { CreateUserDto, LoginDto } from '../dto'
import { User } from '../entities/user.entity'
import * as bcrypt from 'bcrypt'
jest.mock('bcrypt')
describe('AuthService', () => {
let service: AuthService
let usersService: UsersService
let jwtService: JwtService
const mockUser: User = {
id: 1,
email: '[email protected]',
fullName: 'Test User',
password: 'hashedPassword',
role: 'user',
isActive: true,
createdAt: new Date(),
updatedAt: new Date()
}
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{
provide: UsersService,
useValue: {
findByEmail: jest.fn(),
create: jest.fn(),
findOne: jest.fn(),
updateLastLogin: jest.fn(),
update: jest.fn()
}
},
{
provide: JwtService,
useValue: {
sign: jest.fn()
}
}
]
}).compile()
service = module.get<AuthService>(AuthService)
usersService = module.get<UsersService>(UsersService)
jwtService = module.get<JwtService>(JwtService)
(bcrypt.compare as jest.Mock).mockResolvedValue(true)
(bcrypt.hash as jest.Mock).mockResolvedValue('hashedPassword')
(bcrypt.genSalt as jest.Mock).mockResolvedValue('salt')
})
describe('validateUser', () => {
it('should return user when credentials are valid', async () => {
usersService.findByEmail.mockResolvedValue(mockUser)
const result = await service.validateUser('[email protected]', 'password')
expect(result).toEqual(mockUser)
expect(usersService.findByEmail).toHaveBeenCalledWith('[email protected]')
expect(bcrypt.compare).toHaveBeenCalledWith('password', 'hashedPassword')
})
it('should throw UnauthorizedException when user does not exist', async () => {
usersService.findByEmail.mockResolvedValue(null)
await expect(service.validateUser('[email protected]', 'password'))
.rejects.toThrow(UnauthorizedException)
})
it('should throw UnauthorizedException when password is invalid', async () => {
usersService.findByEmail.mockResolvedValue(mockUser)
;(bcrypt.compare as jest.Mock).mockResolvedValue(false)
await expect(service.validateUser('[email protected]', 'wrongpassword'))
.rejects.toThrow(UnauthorizedException)
})
})
describe('login', () => {
it('should return access token and user when credentials are valid', async () => {
const loginDto: LoginDto = { email: '[email protected]', password: 'password' }
const expectedPayload = { sub: 1, email: '[email protected]', role: 'user', name: 'Test User' }
usersService.findByEmail.mockResolvedValue(mockUser)
jwtService.sign.mockReturnValue('jwt-token')
const result = await service.login(loginDto)
expect(result).toEqual({
access_token: 'jwt-token',
user: {
id: 1,
email: '[email protected]',
fullName: 'Test User',
role: 'user'
}
})
expect(jwtService.sign).toHaveBeenCalledWith(expectedPayload)
})
it('should throw UnauthorizedException when account is deactivated', async () => {
const deactivatedUser = { ...mockUser, isActive: false }
const loginDto: LoginDto = { email: '[email protected]', password: 'password' }
usersService.findByEmail.mockResolvedValue(deactivatedUser)
await expect(service.login(loginDto))
.rejects.toThrow(UnauthorizedException)
})
})
describe('register', () => {
it('should return access token and user when registration is successful', async () => {
const createUserDto: CreateUserDto = {
email: '[email protected]',
password: 'password123',
fullName: 'New User'
}
usersService.findByEmail.mockResolvedValue(null)
usersService.create.mockResolvedValue({ id: 2, ...createUserDto })
jwtService.sign.mockReturnValue('new-jwt-token')
const result = await service.register(createUserDto)
expect(result).toEqual({
access_token: 'new-jwt-token',
user: {
id: 2,
email: '[email protected]',
fullName: 'New User',
role: 'user'
}
})
})
it('should throw BadRequestException when email already exists', async () => {
const createUserDto: CreateUserDto = {
email: '[email protected]',
password: 'password123',
fullName: 'Existing User'
}
usersService.findByEmail.mockResolvedValue(mockUser)
await expect(service.register(createUserDto))
.rejects.toThrow(BadRequestException)
})
})
})
// 6. E2E Testing
// test/auth.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'
import { getRepositoryToken } from '@nestjs/typeorm'
import { Repository } from 'typeorm'
import { User } from '../src/users/entities/user.entity'
describe('Authentication (e2e)', () => {
let app: INestApplication
let userRepository: Repository<User>
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile()
app = moduleFixture.createNestApplication()
userRepository = moduleFixture.get<Repository<User>>(getRepositoryToken(User))
await app.init()
})
afterEach(async () => {
await userRepository.clear()
await app.close()
})
describe('/auth/register', () => {
it('should register a new user successfully', () => {
const createUserDto = {
email: '[email protected]',
password: 'password123',
fullName: 'Test User'
}
return request(app.getHttpServer())
.post('/auth/register')
.send(createUserDto)
.expect(201)
.expect((res) => {
expect(res.body.access_token).toBeDefined()
expect(res.body.user.email).toBe(createUserDto.email)
expect(res.body.user.fullName).toBe(createUserDto.fullName)
})
})
it('should return 400 when email already exists', () => {
const createUserDto = {
email: '[email protected]',
password: 'password123',
fullName: 'Duplicate User'
}
// Create first user
request(app.getHttpServer())
.post('/auth/register')
.send(createUserDto)
.expect(201)
// Try to create second user with same email
return request(app.getHttpServer())
.post('/auth/register')
.send(createUserDto)
.expect(400)
})
})
describe('/auth/login', () => {
beforeEach(async () => {
// Create a user for login testing
const createUserDto = {
email: '[email protected]',
password: 'password123',
fullName: 'Login Test User'
}
await request(app.getHttpServer())
.post('/auth/register')
.send(createUserDto)
.expect(201)
})
it('should login successfully with valid credentials', () => {
const loginDto = {
email: '[email protected]',
password: 'password123'
}
return request(app.getHttpServer())
.post('/auth/login')
.send(loginDto)
.expect(200)
.expect((res) => {
expect(res.body.access_token).toBeDefined()
expect(res.body.user.email).toBe(loginDto.email)
})
})
it('should return 401 with invalid credentials', () => {
const loginDto = {
email: '[email protected]',
password: 'wrongpassword'
}
return request(app.getHttpServer())
.post('/auth/login')
.send(loginDto)
.expect(401)
})
})
describe('/auth/profile', () => {
let accessToken: string
beforeEach(async () => {
// Register and login to get access token
const createUserDto = {
email: '[email protected]',
password: 'password123',
fullName: 'Profile Test User'
}
const registerResponse = await request(app.getHttpServer())
.post('/auth/register')
.send(createUserDto)
accessToken = registerResponse.body.access_token
})
it('should return user profile with valid token', () => {
return request(app.getHttpServer())
.get('/auth/profile')
.set('Authorization', `Bearer ${accessToken}`)
.expect(200)
.expect((res) => {
expect(res.body.success).toBe(true)
expect(res.body.data.email).toBe('[email protected]')
})
})
it('should return 401 without token', () => {
return request(app.getHttpServer())
.get('/auth/profile')
.expect(401)
})
it('should return 401 with invalid token', () => {
return request(app.getHttpServer())
.get('/auth/profile')
.set('Authorization', 'Bearer invalid-token')
.expect(401)
})
})
})
// 7. Integration Testing with Test Database
// test/auth.integration.spec.ts
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication } from '@nestjs/common'
import * as request from 'supertest'
import { AppModule } from '../src/app.module'
import { TypeOrmModule } from '@nestjs/typeorm'
import { User } from '../src/users/entities/user.entity'
import { getRepositoryToken } from '@nestjs/typeorm'
describe('Authentication Integration', () => {
let app: INestApplication
let userRepository: Repository<User>
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
AppModule,
TypeOrmModule.forRoot({
type: 'sqlite',
database: ':memory:',
entities: [User],
synchronize: true,
logging: false
})
]
}).compile()
app = moduleFixture.createNestApplication()
userRepository = moduleFixture.get<Repository<User>>(getRepositoryToken(User))
await app.init()
})
afterAll(async () => {
await app.close()
})
describe('Complete Authentication Flow', () => {
it('should handle complete user registration and login flow', async () => {
const userDto = {
email: '[email protected]',
password: 'StrongPassword123!',
fullName: 'Integration Test User'
}
// Step 1: Register user
const registerResponse = await request(app.getHttpServer())
.post('/auth/register')
.send(userDto)
.expect(201)
const { access_token, user } = registerResponse.body
// Step 2: Verify registration response
expect(access_token).toBeDefined()
expect(user.email).toBe(userDto.email)
expect(user.fullName).toBe(userDto.fullName)
// Step 3: Login with registered credentials
const loginResponse = await request(app.getHttpServer())
.post('/auth/login')
.send({
email: userDto.email,
password: userDto.password
})
.expect(200)
// Step 4: Verify login response
expect(loginResponse.body.access_token).toBeDefined()
expect(loginResponse.body.user.email).toBe(userDto.email)
// Step 5: Access protected route
const profileResponse = await request(app.getHttpServer())
.get('/auth/profile')
.set('Authorization', `Bearer ${access_token}`)
.expect(200)
expect(profileResponse.body.data.email).toBe(userDto.email)
// Step 6: Verify user exists in database
const dbUser = await userRepository.findOne({
where: { email: userDto.email }
})
expect(dbUser).toBeDefined()
expect(dbUser.fullName).toBe(userDto.fullName)
expect(dbUser.password).not.toBe(userDto.password) // Password should be hashed
})
it('should handle password change', async () => {
// Register user
const userDto = {
email: '[email protected]',
password: 'OriginalPassword123!',
fullName: 'Password Test User'
}
const registerResponse = await request(app.getHttpServer())
.post('/auth/register')
.send(userDto)
.expect(201)
const { access_token } = registerResponse.body
// Change password
const changePasswordResponse = await request(app.getHttpServer())
.post('/auth/change-password')
.set('Authorization', `Bearer ${access_token}`)
.send({
currentPassword: 'OriginalPassword123!',
newPassword: 'NewPassword456!'
})
.expect(200)
// Try login with old password (should fail)
await request(app.getHttpServer())
.post('/auth/login')
.send({
email: userDto.email,
password: 'OriginalPassword123!'
})
.expect(401)
// Try login with new password (should succeed)
const loginResponse = await request(app.getHttpServer())
.post('/auth/login')
.send({
email: userDto.email,
password: 'NewPassword456!'
})
.expect(200)
expect(loginResponse.body.access_token).toBeDefined()
})
})
})
// 8. Mock Testing with Jest Mocks
// src/auth/auth.service.mock.spec.ts
import { Test, TestingModule } from '@nestjs/testing'
import { AuthService } from './auth.service'
import { UsersService } from '../users/users.service'
import { JwtService } from '@nestjs/jwt'
describe('AuthService (Mocked)', () => {
let service: AuthService
let mockUsersService: Partial<UsersService>
let mockJwtService: Partial<JwtService>
beforeEach(async () => {
mockUsersService = {
findByEmail: jest.fn(),
create: jest.fn(),
findOne: jest.fn(),
updateLastLogin: jest.fn(),
update: jest.fn()
}
mockJwtService = {
sign: jest.fn()
}
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{
provide: UsersService,
useValue: mockUsersService
},
{
provide: JwtService,
useValue: mockJwtService
}
]
}).compile()
service = module.get<AuthService>(AuthService)
})
describe('login', () => {
it('should handle successful login with mocked dependencies', async () => {
const mockUser = {
id: 1,
email: '[email protected]',
password: 'hashedPassword',
isActive: true,
role: 'user'
}
mockUsersService.findByEmail!.mockResolvedValue(mockUser)
mockJwtService.sign!.mockReturnValue('mock-jwt-token')
const result = await service.login({
email: '[email protected]',
password: 'password'
})
expect(mockUsersService.findByEmail).toHaveBeenCalledWith('[email protected]')
expect(mockJwtService.sign).toHaveBeenCalledWith({
sub: 1,
email: '[email protected]',
role: 'user'
})
expect(result).toEqual({
access_token: 'mock-jwt-token',
user: {
id: 1,
email: '[email protected]',
fullName: mockUser.fullName,
role: 'user'
}
})
})
})
})