Skip to content

Chapter 2 - Validasi dan Error Handling

Ikhtisar Chapter

Chapter ini membuat API dari chapter sebelumnya menjadi lebih aman dan lebih ramah client. Fokusnya adalah memastikan input yang masuk sesuai aturan, lalu mengembalikan error HTTP yang tepat ketika data tidak valid atau resource tidak ditemukan.

Peta Cepat

FokusPipes, ValidationPipe, class-validator, exception
Masalah yang DiselesaikanInput kosong, status tidak valid, task tidak ditemukan
Output BelajarDTO yang dapat divalidasi dan service yang melempar exception tepat

Gambaran Besar

Sebelum chapter ini, API dapat menerima request dan mengembalikan data, tetapi belum punya pagar kuat. Client bisa mengirim title kosong, status sembarang, atau ID yang tidak ada tanpa response yang jelas. Validasi dan error handling adalah lapisan yang membuat API terasa profesional.

NestJS menyediakan pipes untuk memproses argumen handler sebelum method controller dijalankan. ValidationPipe bekerja bersama class-validator dan class-transformer agar DTO tidak hanya menjadi tipe TypeScript, tetapi juga aturan runtime yang benar-benar memeriksa request.

Alur Validasi

text
Request Body atau Query
  |
  v
DTO class dengan decorator validasi
  |
  v
ValidationPipe memeriksa data sebelum handler dipanggil
  |
  |-- valid: lanjut ke controller dan service
  |
  |-- tidak valid: NestJS mengembalikan 400 Bad Request

Struktur Direktori

Chapter ini tidak menambahkan file baru, tetapi mengubah isi dari beberapa file penting dan menambahkan satu DTO:

src/
├── main.ts                        ← DIUBAH: + useGlobalPipes(new ValidationPipe())
└── tasks/
    ├── dto/
    │   ├── create-task.dto.ts     ← DIUBAH: + @IsNotEmpty() @IsString()
    │   ├── get-tasks-filter.dto.ts ← DIUBAH: + @IsOptional() @IsEnum()
    │   └── update-task-status.dto.ts  ← BARU: membungkus status body
    ├── task.model.ts
    ├── task-status.enum.ts
    ├── tasks.controller.ts        ← DIUBAH: pakai UpdateTaskStatusDto
    ├── tasks.module.ts
    └── tasks.service.ts           ← DIUBAH: + NotFoundException

Package Baru

bash
yarn add class-validator class-transformer

Dua package ini harus diinstal agar decorator validasi berfungsi saat runtime.

Ringkasan Lecture

1. Introduction to NestJS Pipes

Pipe adalah class yang mengimplementasikan interface PipeTransform. Sebelum handler controller dipanggil, NestJS melewatkan argumen melalui pipe yang terdaftar. Ada dua peran utama pipe:

  • Transformasi: mengubah tipe data input, misalnya string menjadi integer
  • Validasi: memastikan input memenuhi aturan tertentu. Jika gagal, pipe melempar exception dan request berhenti

Pipe bisa dipasang di empat level: pada parameter tertentu, handler, controller, atau secara global. Untuk API yang konsisten, global adalah pilihan paling praktis.

typescript
// src/main.ts — mendaftarkan ValidationPipe secara global
import { NestFactory } from '@nestjs/core'
import { ValidationPipe } from '@nestjs/common'
import { AppModule } from './app.module'

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  app.useGlobalPipes(new ValidationPipe())  // semua endpoint ikut tervalidasi
  await app.listen(3000)
}
bootstrap()

Dengan satu baris ini, setiap DTO yang menggunakan decorator class-validator akan otomatis diperiksa sebelum handler dipanggil.

2. ValidationPipe: Creating a Task

Install dua package yang bekerja sama dengan ValidationPipe:

bash
yarn add class-validator class-transformer

Kemudian tambahkan decorator pada CreateTaskDto. Decorator ini adalah metadata yang dibaca ValidationPipe saat runtime:

typescript
// src/tasks/dto/create-task.dto.ts
import { IsNotEmpty, IsString, MinLength } from 'class-validator'

export class CreateTaskDto {
  @IsNotEmpty()           // tidak boleh string kosong ''
  @IsString()
  @MinLength(3)           // minimal 3 karakter
  title: string

  @IsNotEmpty()
  @IsString()
  description: string
}

Sekarang coba kirim request tanpa title:

json
POST /tasks
{ "description": "tanpa judul" }

Response otomatis 400 Bad Request:

json
{
  "statusCode": 400,
  "message": ["title should not be empty"],
  "error": "Bad Request"
}

Tidak perlu menulis satu baris kode pengecekan di controller atau service. Semua ditangani ValidationPipe.

3. Error Handling: Getting a Non-existing Task

Sebelumnya, getTaskById() mengembalikan undefined jika task tidak ditemukan — yang menyebabkan response 200 dengan body {}. Ini membingungkan client. Solusinya adalah melempar NotFoundException agar NestJS otomatis mengembalikan 404:

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

@Injectable()
export class TasksService {
  private tasks: Task[] = []

  getTaskById(id: string): Task {
    const found = this.tasks.find((task) => task.id === id)

    if (!found) {
      throw new NotFoundException(`Task dengan ID "${id}" tidak ditemukan`)
    }

    return found
  }
}

NestJS memiliki built-in HTTP exceptions untuk semua status code umum. Saat exception ini dilempar, NestJS menangkap dan mengubahnya menjadi JSON response yang proper:

json
{
  "statusCode": 404,
  "message": "Task dengan ID \"abc\" tidak ditemukan",
  "error": "Not Found"
}

Tidak perlu try-catch di controller. Exception dari service diteruskan otomatis ke exception filter global NestJS.

4. Error Handling: Deleting a Non-existing Task

Daripada menambahkan logika pengecekan baru ke deleteTask(), kita cukup memanfaatkan getTaskById() yang sudah ada. Karena getTaskById() melempar exception jika task tidak ditemukan, deleteTask() otomatis mewarisi perilaku yang sama:

typescript
// src/tasks/tasks.service.ts
deleteTask(id: string): void {
  // ini akan melempar NotFoundException jika task tidak ada
  const found = this.getTaskById(id)

  // jika sampai di sini, task pasti ada
  this.tasks = this.tasks.filter((task) => task.id !== found.id)
}

Pola ini penting: jangan duplikasi logika pengecekan. Satu method yang dibuat dengan baik bisa dipakai ulang oleh method lain, dan semua mendapat error handling yang konsisten secara gratis.

5. Validation: Update Task Status

Status task tidak boleh string sembarang — hanya OPEN, IN_PROGRESS, atau DONE. Buat DTO khusus untuk update status dengan decorator @IsEnum():

typescript
// src/tasks/dto/update-task-status.dto.ts
import { IsEnum } from 'class-validator'
import { TaskStatus } from '../task.model'

export class UpdateTaskStatusDto {
  @IsEnum(TaskStatus, {
    message: `Status harus salah satu dari: ${Object.values(TaskStatus).join(', ')}`,
  })
  status: TaskStatus
}

Update controller agar menerima DTO ini:

typescript
// src/tasks/tasks.controller.ts
@Patch(':id/status')
updateTaskStatus(
  @Param('id') id: string,
  @Body() updateTaskStatusDto: UpdateTaskStatusDto,
): Task {
  const { status } = updateTaskStatusDto
  return this.tasksService.updateTaskStatus(id, status)
}

Sekarang request { "status": "DONE" } valid, sedangkan { "status": "SELESAI" } akan ditolak dengan 400 dan pesan yang jelas.

Field query parameter bersifat opsional. Menggunakan @IsOptional() memberitahu ValidationPipe bahwa field boleh tidak ada, tetapi jika ada harus memenuhi aturan validator berikutnya:

typescript
// src/tasks/dto/get-tasks-filter.dto.ts
import { IsEnum, IsOptional, IsString } from 'class-validator'
import { TaskStatus } from '../task.model'

export class GetTasksFilterDto {
  @IsOptional()
  @IsEnum(TaskStatus)       // jika dikirim, harus valid enum
  status?: TaskStatus

  @IsOptional()
  @IsString()               // jika dikirim, harus string
  search?: string
}

Catatan penting: @IsOptional() berbeda dengan tanda ? di TypeScript. Tanda ? hanya memberitahu TypeScript bahwa field itu opsional di level tipe — TypeScript menghapus informasi ini saat compile. @IsOptional() adalah metadata runtime yang dibaca ValidationPipe ketika request masuk. Keduanya diperlukan bersama-sama.

Konsep Kunci

KonsepFungsiCatatan Penting
PipeMemproses argumen sebelum handlerBisa transformasi atau validasi
ValidationPipeMemvalidasi object terhadap DTO classPerlu DTO berbentuk class, bukan interface
class-validatorMenyediakan decorator validasiContoh: @IsNotEmpty(), @IsEnum()
class-transformerMembantu transformasi plain object ke classBekerja bersama ValidationPipe
NotFoundExceptionError HTTP 404Cocok untuk resource yang tidak ditemukan
BadRequestExceptionError HTTP 400Biasanya muncul dari validasi input
DTOTempat aturan inputMenjadi kontrak request yang jelas

Pola Validasi DTO

typescript
export class CreateTaskDto {
  @IsNotEmpty()
  title: string

  @IsNotEmpty()
  description: string
}
typescript
export class UpdateTaskStatusDto {
  @IsEnum(TaskStatus)
  status: TaskStatus
}
typescript
export class GetTasksFilterDto {
  @IsOptional()
  @IsEnum(TaskStatus)
  status?: TaskStatus

  @IsOptional()
  @IsString()
  search?: string
}

Pola Error Handling

typescript
const found = this.tasks.find((task) => task.id === id)

if (!found) {
  throw new NotFoundException(`Task dengan ID ${id} tidak ditemukan`)
}

Exception dilempar di service karena service mengetahui aturan business logic. Controller tetap tipis dan hanya meneruskan request ke service.

Jebakan Umum

  • Mengira status?: TaskStatus cukup untuk validasi runtime. Tanda ? hanya membantu TypeScript, bukan request yang datang saat aplikasi berjalan.
  • Menulis validasi manual berulang di controller, padahal DTO dan ValidationPipe bisa membuatnya deklaratif.
  • Mengembalikan undefined ketika data tidak ditemukan. Client akan lebih terbantu dengan 404 yang jelas.
  • Membuat pesan error terlalu teknis atau tidak konsisten.
  • Lupa bahwa interface TypeScript hilang saat runtime, sehingga tidak cocok sebagai target validasi NestJS.

Pertanyaan Reflektif

  1. Mengapa DTO sebaiknya berupa class ketika dipakai bersama ValidationPipe?
  2. Apa bedanya validasi input di DTO dan validasi business rule di service?
  3. Mengapa @IsOptional() tetap diperlukan meskipun property sudah memakai tanda ??
  4. Bagaimana reuse getTaskById() membuat delete task lebih konsisten?
  5. Dalam kasus apa Anda memilih BadRequestException dibanding NotFoundException?