Published on

Panduan Lengkap Nest.js

Panduan Lengkap Nest.js

Konsep Inti

Controllers (Pengontrol)

// Controller Dasar
import { Controller, Get, Post, Body, Param } from '@nestjs/common'

@Controller('users')
export class UsersController {
  @Get()
  findAll() {
    return ['user1', 'user2']
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return `Pengguna ${id}`
  }

  @Post()
  create(@Body() createUserDto: CreateUserDto) {
    return 'Pengguna berhasil dibuat'
  }
}

// DTOs (Objek Transfer Data)
export class CreateUserDto {
  readonly name: string
  readonly email: string
  readonly password: string
}

// Pipe Validasi
import { IsEmail, IsNotEmpty, MinLength } from 'class-validator'

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

  @IsEmail()
  readonly email: string

  @MinLength(6)
  readonly password: string
}

Providers (Layanan)

// Layanan
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>
  ) {}

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

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

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

// Modul
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'

@Module({
  imports: [TypeOrmModule.forFeature([User])],
  providers: [UsersService],
  controllers: [UsersController],
})
export class UsersModule {}

Middleware

// Middleware Kustom
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(`Permintaan ${req.method} ${req.url}`)
    next()
  }
}

// Menerapkan Middleware
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'

@Module({})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('users')
  }
}

Guards (Penjaga)

// Guard Autentikasi
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Observable } from 'rxjs'

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest()
    return this.validateRequest(request)
  }

  private validateRequest(request: any): boolean {
    // Tambahkan logika autentikasi di sini
    return true
  }
}

// Menggunakan Guards
@Controller('users')
@UseGuards(AuthGuard)
export class UsersController {
  // Rute yang dilindungi
}

Interceptors (Pencegat)

// Interceptor Transform
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'

export interface Response<T> {
  data: T
}

@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 })))
  }
}

// Cache Interceptor
@Injectable()
export class CacheInterceptor implements NestInterceptor {
  constructor(private cacheService: CacheService) {}

  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
    const key = this.generateCacheKey(context)
    const cachedResponse = await this.cacheService.get(key)

    if (cachedResponse) {
      return of(cachedResponse)
    }

    return next.handle().pipe(tap((response) => this.cacheService.set(key, response)))
  }
}

Integrasi Database

Konfigurasi TypeORM

// Entitas
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'

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

  @Column()
  name: string

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

  @Column()
  password: string

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

// Konfigurasi Modul
import { Module } from '@nestjs/common'
import { TypeOrmModule } from '@nestjs/typeorm'

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'postgres',
      password: 'password',
      database: 'mydb',
      entities: [User],
      synchronize: true,
    }),
  ],
})
export class AppModule {}

Pola Repository

// Repository Kustom
import { EntityRepository, Repository } from 'typeorm'
import { User } from './user.entity'

@EntityRepository(User)
export class UserRepository extends Repository<User> {
  async findByEmail(email: string): Promise<User> {
    return this.findOne({ where: { email } })
  }

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

Autentikasi

Implementasi JWT

// Layanan Auth
import { Injectable } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'

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

  async validateUser(email: string, password: string): Promise<any> {
    const user = await this.usersService.findByEmail(email)
    if (user && (await bcrypt.compare(password, user.password))) {
      const { password, ...result } = user
      return result
    }
    return null
  }

  async login(user: any) {
    const payload = { email: user.email, sub: user.id }
    return {
      access_token: this.jwtService.sign(payload),
    }
  }
}

// Strategi JWT
import { ExtractJwt, Strategy } from 'passport-jwt'
import { PassportStrategy } from '@nestjs/passport'

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      ignoreExpiration: false,
      secretOrKey: 'kunci-rahasia-anda',
    })
  }

  async validate(payload: any) {
    return { userId: payload.sub, email: payload.email }
  }
}

Penanganan Exception

// Filter Exception Kustom
import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common'
import { 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 status = exception.getStatus()
    const error = exception.getResponse()

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      error: error,
    })
  }
}

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

Validasi dan Pipes

// Pipe Validasi Kustom
import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common'
import { validate } from 'class-validator'
import { plainToClass } from 'class-transformer'

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      return value
    }
    const object = plainToClass(metatype, value)
    const errors = await validate(object)
    if (errors.length > 0) {
      throw new BadRequestException('Validasi gagal')
    }
    return value
  }

  private toValidate(metatype: Function): boolean {
    const types: Function[] = [String, Boolean, Number, Array, Object]
    return !types.includes(metatype)
  }
}

WebSockets

// Gateway
import { WebSocketGateway, SubscribeMessage, MessageBody } from '@nestjs/websockets'

@WebSocketGateway()
export class ChatGateway {
  @SubscribeMessage('message')
  handleMessage(@MessageBody() data: string): string {
    return data
  }

  @SubscribeMessage('joinRoom')
  handleJoinRoom(@MessageBody() data: any) {
    // Tangani logika bergabung dengan ruangan
  }
}

// Koneksi Klien
import { WebSocketGateway, OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets'
import { Socket } from 'socket.io'

@WebSocketGateway()
export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
  handleConnection(client: Socket) {
    console.log(`Klien terhubung: ${client.id}`)
  }

  handleDisconnect(client: Socket) {
    console.log(`Klien terputus: ${client.id}`)
  }
}

Pengujian

// Unit Testing
import { Test, TestingModule } from '@nestjs/testing'
import { UsersController } from './users.controller'
import { UsersService } from './users.service'

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

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

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

  it('harus terdefinisi', () => {
    expect(controller).toBeDefined()
  })

  it('harus mengembalikan array pengguna', async () => {
    const result = ['user1', 'user2']
    jest.spyOn(service, 'findAll').mockImplementation(() => result)

    expect(await controller.findAll()).toBe(result)
  })
})

// Pengujian E2E
import * as request from 'supertest'
import { Test } from '@nestjs/testing'
import { AppModule } from './../src/app.module'
import { INestApplication } from '@nestjs/common'

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

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

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

  it('/users (GET)', () => {
    return request(app.getHttpServer())
      .get('/users')
      .expect(200)
      .expect('Content-Type', /json/)
      .expect([])
  })
})

Sumber Daya

Dokumentasi Resmi

Sumber Belajar

Tools dan Extensions

Sumber Komunitas