Appearance
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 RequestStruktur 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: + NotFoundExceptionPackage Baru
bash
yarn add class-validator class-transformerDua 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-transformerKemudian 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.
6. Validating Task Filtering and Search
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
| Konsep | Fungsi | Catatan Penting |
|---|---|---|
| Pipe | Memproses argumen sebelum handler | Bisa transformasi atau validasi |
| ValidationPipe | Memvalidasi object terhadap DTO class | Perlu DTO berbentuk class, bukan interface |
| class-validator | Menyediakan decorator validasi | Contoh: @IsNotEmpty(), @IsEnum() |
| class-transformer | Membantu transformasi plain object ke class | Bekerja bersama ValidationPipe |
| NotFoundException | Error HTTP 404 | Cocok untuk resource yang tidak ditemukan |
| BadRequestException | Error HTTP 400 | Biasanya muncul dari validasi input |
| DTO | Tempat aturan input | Menjadi 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?: TaskStatuscukup untuk validasi runtime. Tanda?hanya membantu TypeScript, bukan request yang datang saat aplikasi berjalan. - Menulis validasi manual berulang di controller, padahal DTO dan
ValidationPipebisa membuatnya deklaratif. - Mengembalikan
undefinedketika 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
- Mengapa DTO sebaiknya berupa class ketika dipakai bersama
ValidationPipe? - Apa bedanya validasi input di DTO dan validasi business rule di service?
- Mengapa
@IsOptional()tetap diperlukan meskipun property sudah memakai tanda?? - Bagaimana reuse
getTaskById()membuat delete task lebih konsisten? - Dalam kasus apa Anda memilih
BadRequestExceptiondibandingNotFoundException?