Membangun REST API Type-Safe dengan NestJS dan Prisma

Foto oleh Unsplash

Foto oleh Unsplash
Error runtime di API REST produksi memalukan dan dapat dihindari. Di Commsult, kami membangun API ERP internal yang menangani persetujuan penggajian, pembuatan invoice, dan pergerakan inventaris — kesalahan memiliki biaya nyata. Kombinasi NestJS (untuk struktur), Prisma (untuk akses DB yang type-safe), dan class-validator (untuk validasi request) membentuk jaring pengaman tiga lapis yang mendorong error ke compile time dan test suite, bukan ke log produksi.
NestJS memberikan dependency injection gaya Angular dan decorator yang membuat controller, service, dan modul menjadi eksplisit dan dapat diuji. Prisma menghasilkan client yang sepenuhnya bertipe dari schema.prisma — setiap hasil query memiliki tipe TypeScript, dan query yang tidak mungkin gagal saat compile time. class-validator dengan class-transformer memvalidasi body request yang masuk menggunakan decorator, sehingga field wajib yang hilang atau harga negatif mengembalikan 400 dengan pesan error deskriptif.
Memahami siklus hidup request NestJS sangat penting untuk menempatkan logika dengan benar. Guard berjalan pertama — mereka menangani autentikasi dan otorisasi. Pipe berjalan berikutnya — di sinilah validasi DTO terjadi melalui ValidationPipe. Kemudian metode controller berjalan, mendelegasikan ke service, yang memanggil Prisma. Akhirnya, interceptor mengubah respons.
Schema.prisma Prisma-mu adalah definisi kanonik model data-mu. Dari situ, Prisma menghasilkan tipe TypeScript untuk setiap model, setiap relasi, dan setiap hasil query. Ketika kamu mengubah skema — menambahkan field, mengganti nama kolom — menjalankan 'npx prisma generate' meregenerasi client dan TypeScript segera menandai kode apapun yang menggunakan nama field lama.
HTTP Request
│
▼
┌─────────────────┐
│ NestJS Guard │ (JWT validation, role check)
└────────┬────────┘
│
▼
┌─────────────────┐
│ Pipe / DTO │ (class-validator + class-transformer)
│ Validation │ (400 Bad Request on failure)
└────────┬────────┘
│
▼
┌─────────────────┐
│ Controller │ (@Get, @Post, @Put, @Delete)
└────────┬────────┘
│
▼
┌─────────────────┐
│ Service │ (business logic, type-safe)
└────────┬────────┘
│
▼
┌─────────────────┐
│ Prisma Client │ (fully typed DB queries)
└────────┬────────┘
│
▼
PostgreSQL DBAktifkan logging query Prisma di development: atur log: ['query'] di opsi PrismaClient. Kamu akan melihat SQL sebenarnya yang dieksekusi, yang mengungkap masalah N+1 query secara langsung. Metode service yang memanggil findMany pada pesanan dan kemudian loop untuk menemukan setiap pelanggan secara individual menjadi jelas ketika kamu melihat 21 query SQL berurutan untuk 20 pesanan.
Data Transfer Object (DTO) adalah kelas TypeScript biasa yang didekorasi dengan decorator class-validator. Mereka mendefinisikan dengan tepat bentuk apa yang harus dimiliki body request. ValidationPipe NestJS mencegat request yang masuk, menjalankan class-validator pada body, dan secara otomatis mengembalikan 400 dengan pesan error tingkat field jika validasi gagal. Aktifkan secara global di main.ts dengan app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true })).
Buat PrismaService khusus yang memperluas PrismaClient dan mengimplementasikan OnModuleInit dan OnModuleDestroy. Ini memastikan koneksi dibuka sekali ketika modul diinisialisasi dan ditutup dengan baik saat shutdown. Injeksi melalui dependency injection NestJS di mana pun kamu membutuhkan akses database. Jangan pernah menginstansiasi PrismaClient langsung di service — kamu akan menghabiskan connection pool di bawah beban.
// prisma/schema.prisma
model Product {
id String @id @default(cuid())
name String
price Decimal @db.Decimal(10, 2)
stock Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
categoryId String
category Category @relation(fields: [categoryId], references: [id])
}
// src/products/dto/create-product.dto.ts
import { IsString, IsNumber, IsPositive, IsNotEmpty, Min } from "class-validator"
import { Type } from "class-transformer"
export class CreateProductDto {
@IsString()
@IsNotEmpty()
name: string
@IsNumber({ maxDecimalPlaces: 2 })
@IsPositive()
@Type(() => Number)
price: number
@IsNumber()
@Min(0)
@Type(() => Number)
stock: number
@IsString()
@IsNotEmpty()
categoryId: string
}
// src/products/products.service.ts
import { Injectable, NotFoundException } from "@nestjs/common"
import { PrismaService } from "../prisma/prisma.service"
import { CreateProductDto } from "./dto/create-product.dto"
import { Prisma } from "@prisma/client"
@Injectable()
export class ProductsService {
constructor(private readonly prisma: PrismaService) {}
async create(dto: CreateProductDto) {
return this.prisma.product.create({
data: dto,
include: { category: true },
})
}
async findAll(page = 1, limit = 20) {
const skip = (page - 1) * limit
const [data, total] = await this.prisma.$transaction([
this.prisma.product.findMany({
skip,
take: limit,
include: { category: { select: { name: true } } },
orderBy: { createdAt: "desc" },
}),
this.prisma.product.count(),
])
return { data, total, page, limit, pages: Math.ceil(total / limit) }
}
async findOne(id: string) {
const product = await this.prisma.product.findUnique({
where: { id },
include: { category: true },
})
if (!product) throw new NotFoundException(`Product ${id} not found`)
return product
}
}Prisma melempar error bertipe: PrismaClientKnownRequestError dengan kode P2025 berarti 'record tidak ditemukan', P2002 berarti 'pelanggaran unique constraint'. Tangkap ini di service-mu dan lempar exception NestJS yang sesuai (NotFoundException, ConflictException). Gunakan exception filter untuk menstandarkan respons error: setiap error harus mengembalikan { statusCode, message, error, timestamp, path }.
Pesan error Prisma dapat berisi nama tabel, nama kolom, dan detail query yang mengungkap skema database-mu. Selalu tangkap error Prisma di layer service-mu, log error lengkap di sisi server, dan lempar exception HTTP NestJS yang sudah dibersihkan ke klien. PrismaClientKnownRequestError mentah dalam respons 500 adalah masalah keamanan, bukan hanya masalah UX.
Stack type-safety bersinar dalam pengujian. Gunakan @nestjs/testing untuk membuat modul pengujian, mock PrismaService dengan jest.mock, dan uji setiap metode service secara terpisah. Karena tipe Prisma dihasilkan dari skema-mu, nilai kembalian yang di-mock harus sesuai dengan tipe nyata — TypeScript menangkap bentuk mock yang salah saat compile time.
Saat API berkembang, perkenalkan pola repository antara service dan Prisma. Repository menangani query Prisma mentah; service menangani logika bisnis. Pemisahan ini membuat service lebih mudah diuji dan memungkinkan kamu mengganti layer database tanpa menulis ulang logika bisnis. Untuk query kompleks dengan banyak relasi, gunakan select dan include Prisma dengan hemat — definisikan objek select yang dapat digunakan kembali sebagai konstanta TypeScript.