# NestJS + Prisma: stack backend modern untuk Node.js > Panduan lengkap untuk membangun API backend modern dengan NestJS dan Prisma. Setup, model, service, transaksi, dan praktik terbaik dijelaskan. - Published: 2026-02-09 - Updated: 2026-04-28 - Author: SharpSkill - Tags: nestjs, prisma, nodejs, typescript, backend - Reading time: 14 min --- NestJS dan Prisma membentuk kombinasi yang kuat untuk pengembangan backend modern. NestJS menyediakan arsitektur modular dan dependency injection, sementara Prisma menghadirkan ORM yang aman secara tipe dengan pengalaman developer yang luar biasa. Stack ini memungkinkan pembangunan API yang andal dan mudah dipelihara. > **Mengapa kombinasi ini** > > Prisma secara otomatis menghasilkan klien TypeScript bertipe dari skema database. Dipadukan dengan NestJS dan sistem modulnya, kode mendokumentasikan dirinya sendiri dan kesalahan tipe terdeteksi pada saat kompilasi. ## Konfigurasi awal proyek Mengintegrasikan Prisma ke dalam proyek NestJS memerlukan beberapa langkah konfigurasi. Prosesnya standar dan terdokumentasi dengan baik. ```bash # terminal # Create a new NestJS project nest new my-backend-api cd my-backend-api # Install Prisma as a dev dependency npm install prisma --save-dev # Install the Prisma client npm install @prisma/client # Initialize Prisma with PostgreSQL npx prisma init --datasource-provider postgresql ``` Perintah ini membuat folder `prisma/` dengan file `schema.prisma` dan file `.env` untuk variabel lingkungan. ```typescript // src/prisma/prisma.service.ts import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { // Dedicated logger for Prisma operations private readonly logger = new Logger(PrismaService.name); constructor() { // Configure client with logging in development super({ log: process.env.NODE_ENV === 'development' ? ['query', 'info', 'warn', 'error'] : ['error'], }); } // Automatic connection on module startup async onModuleInit() { await this.$connect(); this.logger.log('Prisma connection established'); } // Clean disconnection on application shutdown async onModuleDestroy() { await this.$disconnect(); this.logger.log('Prisma connection closed'); } } ``` Service Prisma kini siap untuk diinjeksi ke service lain pada aplikasi. ## Membuat modul Prisma global Untuk membuat service Prisma tersedia di seluruh aplikasi tanpa perlu mengimpornya secara eksplisit di setiap modul, digunakan dekorator `@Global()`. ```typescript // src/prisma/prisma.module.ts import { Global, Module } from '@nestjs/common'; import { PrismaService } from './prisma.service'; // The @Global decorator makes this module available everywhere @Global() @Module({ providers: [PrismaService], exports: [PrismaService], }) export class PrismaModule {} ``` Impor pada modul root membuat service tersedia secara otomatis. ```typescript // src/app.module.ts import { Module } from '@nestjs/common'; import { PrismaModule } from './prisma/prisma.module'; import { UsersModule } from './users/users.module'; import { PostsModule } from './posts/posts.module'; @Module({ imports: [ PrismaModule, // Single declaration is sufficient UsersModule, PostsModule, ], }) export class AppModule {} ``` Kini setiap service dapat menginjeksi `PrismaService` tanpa impor tambahan. > **Modul global vs impor eksplisit** > > Modul global menyederhanakan arsitektur tetapi membuat ketergantungan menjadi implisit. Untuk aplikasi kecil hal ini dapat diterima. Pada proyek besar, impor eksplisit memperbaiki keterlacakan ketergantungan. ## Mendefinisikan skema Prisma File `schema.prisma` mendefinisikan model data, relasi, dan opsi database. Prisma menggunakan bahasa definisi skemanya sendiri (PSL). ```prisma // prisma/schema.prisma generator client { provider = "prisma-client-js" // Enable types for advanced filtering queries previewFeatures = ["fullTextSearch"] } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // User model with all relations model User { id String @id @default(cuid()) email String @unique password String name String role Role @default(USER) createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // One-to-many relations posts Post[] comments Comment[] profile Profile? // Index for frequent searches @@index([email]) @@map("users") } // Enum for user roles enum Role { USER ADMIN MODERATOR } // One-to-one relation with User model Profile { id String @id @default(cuid()) bio String? avatar String? website String? userId String @unique @map("user_id") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("profiles") } // Posts with many-to-one relation to User model Post { id String @id @default(cuid()) title String slug String @unique content String excerpt String? published Boolean @default(false) publishedAt DateTime? @map("published_at") authorId String @map("author_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations author User @relation(fields: [authorId], references: [id], onDelete: Cascade) comments Comment[] categories CategoriesOnPosts[] @@index([authorId]) @@index([slug]) @@map("posts") } // Comments with dual relation model Comment { id String @id @default(cuid()) content String postId String @map("post_id") authorId String @map("author_id") createdAt DateTime @default(now()) @map("created_at") post Post @relation(fields: [postId], references: [id], onDelete: Cascade) author User @relation(fields: [authorId], references: [id], onDelete: Cascade) @@index([postId]) @@map("comments") } // Categories for posts model Category { id String @id @default(cuid()) name String @unique slug String @unique posts CategoriesOnPosts[] @@map("categories") } // Pivot table for many-to-many relation model CategoriesOnPosts { postId String @map("post_id") categoryId String @map("category_id") assignedAt DateTime @default(now()) @map("assigned_at") post Post @relation(fields: [postId], references: [id], onDelete: Cascade) category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade) @@id([postId, categoryId]) @@map("categories_on_posts") } ``` Setelah skema diubah, migrasi menerapkan perubahan tersebut ke database. ```bash # terminal # Create a migration with descriptive name npx prisma migrate dev --name init_schema # Generate Prisma client (automatic after migrate dev) npx prisma generate # View schema in browser npx prisma studio ``` ## Implementasi service users dengan Prisma Service Users menggambarkan operasi CRUD yang umum dengan Prisma. Pengetikan otomatis menjamin konsistensi antara kode dan database. ```typescript // src/users/users.service.ts import { Injectable, NotFoundException, ConflictException, BadRequestException, } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User, Prisma } from '@prisma/client'; import * as bcrypt from 'bcrypt'; // Type for results without password type SafeUser = Omit; @Injectable() export class UsersService { constructor(private readonly prisma: PrismaService) {} // Default selection without password private readonly safeSelect: Prisma.UserSelect = { id: true, email: true, name: true, role: true, createdAt: true, updatedAt: true, profile: true, }; async create(createUserDto: CreateUserDto): Promise { // Check email uniqueness const existingUser = await this.prisma.user.findUnique({ where: { email: createUserDto.email }, }); if (existingUser) { throw new ConflictException('This email is already in use'); } // Secure password hashing const hashedPassword = await bcrypt.hash(createUserDto.password, 12); // Creation with optional profile return this.prisma.user.create({ data: { email: createUserDto.email, password: hashedPassword, name: createUserDto.name, // Nested profile creation if provided profile: createUserDto.bio ? { create: { bio: createUserDto.bio, }, } : undefined, }, select: this.safeSelect, }); } async findAll(params: { page?: number; limit?: number; search?: string; }): Promise<{ data: SafeUser[]; total: number; pages: number }> { const { page = 1, limit = 10, search } = params; const skip = (page - 1) * limit; // Optional search condition const where: Prisma.UserWhereInput = search ? { OR: [ { email: { contains: search, mode: 'insensitive' } }, { name: { contains: search, mode: 'insensitive' } }, ], } : {}; // Parallel execution for performance const [data, total] = await this.prisma.$transaction([ this.prisma.user.findMany({ where, skip, take: limit, orderBy: { createdAt: 'desc' }, select: this.safeSelect, }), this.prisma.user.count({ where }), ]); return { data, total, pages: Math.ceil(total / limit), }; } async findOne(id: string): Promise { const user = await this.prisma.user.findUnique({ where: { id }, select: { ...this.safeSelect, // Include recent posts posts: { take: 5, orderBy: { createdAt: 'desc' }, select: { id: true, title: true, slug: true, published: true, }, }, }, }); if (!user) { throw new NotFoundException(`User with ID ${id} not found`); } return user; } async update(id: string, updateUserDto: UpdateUserDto): Promise { // Check existence await this.findOne(id); // Update with nested profile handling return this.prisma.user.update({ where: { id }, data: { name: updateUserDto.name, // Update or create profile profile: updateUserDto.bio ? { upsert: { create: { bio: updateUserDto.bio }, update: { bio: updateUserDto.bio }, }, } : undefined, }, select: this.safeSelect, }); } async remove(id: string): Promise { await this.findOne(id); // Deletion cascades to profile and posts await this.prisma.user.delete({ where: { id } }); } } ``` Pengetikan Prisma menjamin bahwa semua properti yang digunakan ada di dalam skema. ## Mengelola relasi dengan Prisma Prisma menyederhanakan penanganan relasi yang kompleks. Query bersarang memungkinkan memuat data terkait dalam satu permintaan. ```typescript // src/posts/posts.service.ts import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreatePostDto } from './dto/create-post.dto'; import { UpdatePostDto } from './dto/update-post.dto'; import { Prisma } from '@prisma/client'; @Injectable() export class PostsService { constructor(private readonly prisma: PrismaService) {} async create(authorId: string, createPostDto: CreatePostDto) { // Automatic slug generation from title const slug = this.generateSlug(createPostDto.title); return this.prisma.post.create({ data: { title: createPostDto.title, slug, content: createPostDto.content, excerpt: createPostDto.excerpt, // Connect to existing author author: { connect: { id: authorId }, }, // Connect to existing categories categories: createPostDto.categoryIds ? { create: createPostDto.categoryIds.map(categoryId => ({ category: { connect: { id: categoryId } }, })), } : undefined, }, include: { author: { select: { id: true, name: true }, }, categories: { include: { category: true, }, }, }, }); } async findAllPublished(params: { page?: number; limit?: number; categorySlug?: string; }) { const { page = 1, limit = 10, categorySlug } = params; const skip = (page - 1) * limit; // Conditional filter by category const where: Prisma.PostWhereInput = { published: true, ...(categorySlug && { categories: { some: { category: { slug: categorySlug }, }, }, }), }; const [posts, total] = await this.prisma.$transaction([ this.prisma.post.findMany({ where, skip, take: limit, orderBy: { publishedAt: 'desc' }, include: { author: { select: { id: true, name: true }, }, categories: { include: { category: { select: { name: true, slug: true } }, }, }, _count: { select: { comments: true }, }, }, }), this.prisma.post.count({ where }), ]); return { posts, total, pages: Math.ceil(total / limit) }; } async findBySlug(slug: string) { const post = await this.prisma.post.findUnique({ where: { slug }, include: { author: { select: { id: true, name: true, profile: true }, }, categories: { include: { category: true, }, }, comments: { orderBy: { createdAt: 'desc' }, take: 20, include: { author: { select: { id: true, name: true }, }, }, }, }, }); if (!post) { throw new NotFoundException(`Post "${slug}" not found`); } return post; } async publish(id: string, authorId: string) { // Verify author is the owner const post = await this.prisma.post.findUnique({ where: { id }, select: { authorId: true }, }); if (!post) { throw new NotFoundException(`Post with ID ${id} not found`); } if (post.authorId !== authorId) { throw new ForbiddenException('Publication not authorized'); } return this.prisma.post.update({ where: { id }, data: { published: true, publishedAt: new Date(), }, }); } private generateSlug(title: string): string { return title .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/[^a-z0-9]+/g, '-') .replace(/(^-|-$)/g, ''); } } ``` Penggunaan `include` memungkinkan pengambilan relasi mendalam dengan mengontrol field yang dikembalikan. ## Transaksi dan operasi atomik Prisma menawarkan beberapa metode untuk menjamin atomisitas operasi. Transaksi interaktif memberikan fleksibilitas tertinggi. ```typescript // src/orders/orders.service.ts import { Injectable, BadRequestException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; import { CreateOrderDto } from './dto/create-order.dto'; @Injectable() export class OrdersService { constructor(private readonly prisma: PrismaService) {} async createOrder(userId: string, createOrderDto: CreateOrderDto) { // Interactive transaction to ensure atomicity return this.prisma.$transaction(async (tx) => { // 1. Verify stock for each item const items = await Promise.all( createOrderDto.items.map(async (item) => { const product = await tx.product.findUnique({ where: { id: item.productId }, }); if (!product) { throw new BadRequestException( `Product ${item.productId} not found` ); } if (product.stock < item.quantity) { throw new BadRequestException( `Insufficient stock for ${product.name}` ); } return { product, quantity: item.quantity }; }) ); // 2. Calculate total const total = items.reduce( (sum, { product, quantity }) => sum + product.price * quantity, 0 ); // 3. Create order const order = await tx.order.create({ data: { userId, total, status: 'PENDING', items: { create: items.map(({ product, quantity }) => ({ productId: product.id, quantity, price: product.price, })), }, }, include: { items: { include: { product: true }, }, }, }); // 4. Update stock await Promise.all( items.map(({ product, quantity }) => tx.product.update({ where: { id: product.id }, data: { stock: { decrement: quantity } }, }) ) ); return order; }); } async cancelOrder(orderId: string, userId: string) { return this.prisma.$transaction(async (tx) => { // Get order with items const order = await tx.order.findUnique({ where: { id: orderId }, include: { items: true }, }); if (!order || order.userId !== userId) { throw new BadRequestException('Order not found'); } if (order.status !== 'PENDING') { throw new BadRequestException( 'Only pending orders can be cancelled' ); } // Restore stock await Promise.all( order.items.map((item) => tx.product.update({ where: { id: item.productId }, data: { stock: { increment: item.quantity } }, }) ) ); // Update status return tx.order.update({ where: { id: orderId }, data: { status: 'CANCELLED' }, }); }); } } ``` Transaksi menjamin bahwa semua operasi berhasil bersama-sama atau gagal bersama-sama. > **Timeout transaksi** > > Secara default, transaksi Prisma memiliki timeout 5 detik. Untuk operasi yang panjang, hal ini dapat disesuaikan dengan `$transaction([...], { timeout: 10000 })`. ## Middleware Prisma untuk audit Middleware Prisma memungkinkan menangkap query untuk menambahkan perilaku lintas potong seperti audit atau soft delete. ```typescript // src/prisma/prisma.service.ts (version with middleware) import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common'; import { PrismaClient, Prisma } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { private readonly logger = new Logger(PrismaService.name); constructor() { super({ log: [ { level: 'query', emit: 'event' }, { level: 'error', emit: 'stdout' }, ], }); // Middleware for modification auditing this.$use(async (params: Prisma.MiddlewareParams, next) => { const start = Date.now(); // Execute query const result = await next(params); const duration = Date.now() - start; // Log slow queries (> 100ms) if (duration > 100) { this.logger.warn( `Slow query: ${params.model}.${params.action} - ${duration}ms` ); } // Audit write operations if (['create', 'update', 'delete'].includes(params.action)) { this.logger.log( `Audit: ${params.action} on ${params.model} - ${duration}ms` ); } return result; }); // Middleware for automatic soft delete this.$use(async (params, next) => { // Transform delete to update for certain models if (params.model === 'User' && params.action === 'delete') { params.action = 'update'; params.args['data'] = { deletedAt: new Date() }; } // Automatic exclusion of deleted records if (params.model === 'User' && params.action === 'findMany') { if (!params.args) params.args = {}; if (!params.args.where) params.args.where = {}; params.args.where.deletedAt = null; } return next(params); }); } async onModuleInit() { await this.$connect(); } async onModuleDestroy() { await this.$disconnect(); } } ``` Middleware dieksekusi sesuai urutan registrasi dan dapat mengubah parameter query. ## Optimasi performa dengan Prisma Beberapa teknik mengoptimalkan performa query Prisma pada aplikasi NestJS. ```typescript // src/common/prisma-extensions.ts import { Prisma, PrismaClient } from '@prisma/client'; // Extension for standardized pagination export const paginationExtension = Prisma.defineExtension({ model: { $allModels: { async paginate( this: T, args: Prisma.Exact> & { page?: number; limit?: number; } ): Promise<{ data: Prisma.Result; meta: { page: number; limit: number; total: number; pages: number }; }> { const { page = 1, limit = 10, ...rest } = args as any; const skip = (page - 1) * limit; const context = Prisma.getExtensionContext(this); const [data, total] = await Promise.all([ (context as any).findMany({ ...rest, skip, take: limit }), (context as any).count({ where: (rest as any).where }), ]); return { data, meta: { page, limit, total, pages: Math.ceil(total / limit), }, }; }, }, }, }); // Usage in service // const result = await this.prisma.$extends(paginationExtension) // .user.paginate({ page: 2, limit: 20, where: { role: 'USER' } }); ``` Untuk query yang sering dijalankan, cache meningkatkan waktu respons secara signifikan. ```typescript // src/posts/posts.service.ts (with cache) import { Injectable, Inject } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { PrismaService } from '../prisma/prisma.service'; @Injectable() export class PostsService { constructor( private readonly prisma: PrismaService, @Inject(CACHE_MANAGER) private cacheManager: Cache, ) {} async findPopularPosts() { const cacheKey = 'posts:popular'; // Try to get from cache const cached = await this.cacheManager.get(cacheKey); if (cached) { return cached; } // Database query const posts = await this.prisma.post.findMany({ where: { published: true }, orderBy: { comments: { _count: 'desc' } }, take: 10, include: { author: { select: { name: true } }, _count: { select: { comments: true } }, }, }); // Cache for 5 minutes await this.cacheManager.set(cacheKey, posts, 300000); return posts; } async invalidateCache(postId: string) { // Selective cache invalidation await this.cacheManager.del('posts:popular'); await this.cacheManager.del(`post:${postId}`); } } ``` ## Pengujian dengan Prisma dan NestJS Pengujian memerlukan strategi database yang terisolasi. Menggunakan database khusus pengujian menjamin reproduktibilitas. ```typescript // test/helpers/prisma-test.helper.ts import { PrismaClient } from '@prisma/client'; import { execSync } from 'child_process'; const prisma = new PrismaClient(); export async function setupTestDatabase() { // Use a test database process.env.DATABASE_URL = process.env.TEST_DATABASE_URL; // Apply migrations execSync('npx prisma migrate deploy', { env: { ...process.env, DATABASE_URL: process.env.TEST_DATABASE_URL }, }); } export async function cleanupTestDatabase() { // Delete all data in dependency order const tablenames = await prisma.$queryRaw>` SELECT tablename FROM pg_tables WHERE schemaname='public' `; for (const { tablename } of tablenames) { if (tablename !== '_prisma_migrations') { await prisma.$executeRawUnsafe( `TRUNCATE TABLE "public"."${tablename}" CASCADE;` ); } } } export async function disconnectTestDatabase() { await prisma.$disconnect(); } ``` Pengujian integrasi menggunakan helper ini untuk memastikan lingkungan yang bersih. ```typescript // test/users.e2e-spec.ts import { Test, TestingModule } from '@nestjs/testing'; import { INestApplication, ValidationPipe } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from '../src/app.module'; import { PrismaService } from '../src/prisma/prisma.service'; import { cleanupTestDatabase, setupTestDatabase } from './helpers/prisma-test.helper'; describe('UsersController (e2e)', () => { let app: INestApplication; let prisma: PrismaService; beforeAll(async () => { await setupTestDatabase(); const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleFixture.createNestApplication(); app.useGlobalPipes(new ValidationPipe({ whitelist: true })); prisma = app.get(PrismaService); await app.init(); }); beforeEach(async () => { await cleanupTestDatabase(); }); afterAll(async () => { await app.close(); }); describe('POST /users', () => { it('should create a new user', async () => { const createUserDto = { email: 'test@example.com', password: 'Password123!', name: 'Test User', }; const response = await request(app.getHttpServer()) .post('/users') .send(createUserDto) .expect(201); expect(response.body).toMatchObject({ email: createUserDto.email, name: createUserDto.name, }); expect(response.body.password).toBeUndefined(); }); it('should reject duplicate email', async () => { const createUserDto = { email: 'duplicate@example.com', password: 'Password123!', name: 'First User', }; await request(app.getHttpServer()) .post('/users') .send(createUserDto) .expect(201); await request(app.getHttpServer()) .post('/users') .send({ ...createUserDto, name: 'Second User' }) .expect(409); }); }); }); ``` ## Kesimpulan NestJS dan Prisma membentuk stack backend yang modern dan produktif. Pengetikan otomatis, migrasi deklaratif, dan integrasi native dengan NestJS memungkinkan pengembangan API yang andal dengan cepat. ### Daftar periksa untuk integrasi NestJS + Prisma yang sukses - ✅ Modul Prisma global untuk injeksi yang disederhanakan - ✅ Skema Prisma dengan relasi dan indeks yang dioptimalkan - ✅ Service bertipe dengan pemilihan field eksplisit - ✅ Transaksi untuk operasi atomik - ✅ Middleware untuk audit dan soft delete - ✅ Cache untuk query yang sering dijalankan - ✅ Pengujian integrasi dengan database terisolasi - ✅ Paginasi terstandar melalui extensions Kombinasi ini memanfaatkan kekuatan masing-masing alat: arsitektur modular NestJS untuk struktur dan Prisma untuk lapisan data yang aman secara tipe. Hasilnya adalah kode yang mudah dipelihara, dapat diuji, dan berkinerja baik, cocok untuk aplikasi enterprise. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/node-nestjs/nestjs-prisma-modern-backend-stack