# NestJS + Prisma: สแตกแบ็กเอนด์สมัยใหม่สำหรับ Node.js > คู่มือฉบับสมบูรณ์ในการสร้าง API แบ็กเอนด์สมัยใหม่ด้วย NestJS และ Prisma ครอบคลุมการตั้งค่า โมเดล เซอร์วิส ทรานแซกชัน และแนวปฏิบัติที่ดี - Published: 2026-02-09 - Updated: 2026-04-28 - Author: SharpSkill - Tags: nestjs, prisma, nodejs, typescript, backend - Reading time: 14 min --- NestJS และ Prisma เป็นการผสมผสานที่ทรงพลังสำหรับการพัฒนาแบ็กเอนด์สมัยใหม่ NestJS มอบสถาปัตยกรรมแบบโมดูลและการฉีดดีเพนเดนซี ในขณะที่ Prisma มอบ ORM ที่ปลอดภัยด้านชนิดพร้อมประสบการณ์นักพัฒนาที่ยอดเยี่ยม สแตกนี้ช่วยให้สร้าง API ที่แข็งแรงและบำรุงรักษาง่ายได้ > **เหตุผลของการจับคู่นี้** > > Prisma สร้างไคลเอนต์ TypeScript ที่มีชนิดข้อมูลโดยอัตโนมัติจากสคีมาฐานข้อมูล เมื่อรวมกับ NestJS และระบบโมดูล โค้ดจะอธิบายตัวเองได้และข้อผิดพลาดด้านชนิดจะถูกตรวจพบตั้งแต่ตอนคอมไพล์ ## การตั้งค่าโปรเจกต์เริ่มต้น การรวม Prisma เข้ากับโปรเจกต์ NestJS ต้องผ่านขั้นตอนการตั้งค่าเพียงไม่กี่ขั้นตอน กระบวนการเป็นมาตรฐานและมีเอกสารครบถ้วน ```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 ``` คำสั่งนี้จะสร้างโฟลเดอร์ `prisma/` พร้อมไฟล์ `schema.prisma` และไฟล์ `.env` สำหรับตัวแปรสภาพแวดล้อม ```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'); } } ``` เซอร์วิส Prisma พร้อมที่จะถูกฉีดเข้าไปยังเซอร์วิสอื่น ๆ ของแอปพลิเคชันแล้ว ## การสร้างโมดูล Prisma แบบ Global เพื่อทำให้เซอร์วิส Prisma ใช้งานได้ทั่วทั้งแอปพลิเคชันโดยไม่ต้องนำเข้าอย่างชัดเจนในแต่ละโมดูล จึงใช้เดคอเรเตอร์ `@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 {} ``` การนำเข้าในโมดูลรากทำให้เซอร์วิสพร้อมใช้งานโดยอัตโนมัติ ```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 {} ``` ตอนนี้เซอร์วิสใดก็สามารถฉีด `PrismaService` ได้โดยไม่ต้องนำเข้าเพิ่มเติม > **โมดูล Global เทียบกับการนำเข้าแบบชัดเจน** > > โมดูล Global ช่วยลดความซับซ้อนของสถาปัตยกรรม แต่ทำให้ดีเพนเดนซีกลายเป็นแบบซ่อน สำหรับแอปพลิเคชันขนาดเล็กก็ยอมรับได้ ในโปรเจกต์ขนาดใหญ่ การนำเข้าแบบชัดเจนช่วยให้ติดตามดีเพนเดนซีได้ง่ายขึ้น ## การกำหนดสคีมา Prisma ไฟล์ `schema.prisma` กำหนดโมเดลข้อมูล ความสัมพันธ์ และตัวเลือกของฐานข้อมูล Prisma ใช้ภาษานิยามสคีมาของตัวเอง (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") } ``` หลังจากแก้ไขสคีมาแล้ว มายเกรชันจะนำการเปลี่ยนแปลงไปใช้กับฐานข้อมูล ```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 ``` ## การพัฒนาเซอร์วิสผู้ใช้ด้วย Prisma เซอร์วิส Users แสดงให้เห็นถึงการดำเนินการ CRUD ทั่วไปกับ Prisma การกำหนดชนิดอัตโนมัติช่วยให้โค้ดและฐานข้อมูลสอดคล้องกัน ```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 } }); } } ``` การกำหนดชนิดของ Prisma ช่วยรับประกันว่าทุกคุณสมบัติที่ใช้มีอยู่ในสคีมา ## การจัดการความสัมพันธ์ด้วย Prisma Prisma ทำให้การจัดการความสัมพันธ์ที่ซับซ้อนเป็นเรื่องง่าย คิวรีแบบซ้อนช่วยให้โหลดข้อมูลที่เกี่ยวข้องได้ในคำขอครั้งเดียว ```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, ''); } } ``` `include` ช่วยให้ดึงความสัมพันธ์เชิงลึกได้พร้อมควบคุมว่าจะส่งคืนฟิลด์ใดบ้าง ## ทรานแซกชันและการดำเนินงานแบบอะตอมิก Prisma มีหลายวิธีในการรับประกันความเป็นอะตอมิกของการดำเนินงาน ทรานแซกชันแบบอินเทอร์แอคทีฟให้ความยืดหยุ่นมากที่สุด ```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' }, }); }); } } ``` ทรานแซกชันรับประกันว่าทุกการดำเนินงานจะสำเร็จร่วมกันหรือล้มเหลวร่วมกัน > **ระยะหมดเวลาของทรานแซกชัน** > > โดยค่าเริ่มต้น ทรานแซกชันของ Prisma มีระยะหมดเวลา 5 วินาที สำหรับการดำเนินงานที่ใช้เวลานาน สามารถปรับได้ด้วย `$transaction([...], { timeout: 10000 })` ## มิดเดิลแวร์ของ Prisma สำหรับการตรวจสอบ มิดเดิลแวร์ของ Prisma ช่วยให้สามารถดักจับคิวรีเพื่อเพิ่มพฤติกรรมแบบขวาง เช่น การตรวจสอบหรือ 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(); } } ``` มิดเดิลแวร์จะทำงานตามลำดับที่ลงทะเบียนและสามารถแก้ไขพารามิเตอร์ของคิวรีได้ ## การปรับปรุงประสิทธิภาพด้วย Prisma มีหลายเทคนิคที่ช่วยปรับปรุงประสิทธิภาพของคิวรี Prisma ในแอปพลิเคชัน 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' } }); ``` สำหรับคิวรีที่เรียกใช้บ่อย แคชจะช่วยปรับปรุงเวลาตอบสนองอย่างมีนัยสำคัญ ```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}`); } } ``` ## การทดสอบด้วย Prisma และ NestJS การทดสอบต้องใช้กลยุทธ์ฐานข้อมูลแบบแยกอิสระ การใช้ฐานข้อมูลเฉพาะสำหรับการทดสอบช่วยรับประกันความสามารถในการทำซ้ำ ```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(); } ``` การทดสอบเชิงรวมใช้ตัวช่วยเหล่านี้เพื่อรับประกันสภาพแวดล้อมที่สะอาด ```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); }); }); }); ``` ## บทสรุป NestJS และ Prisma ก่อให้เกิดสแตกแบ็กเอนด์ที่ทันสมัยและมีผลิตภาพสูง การกำหนดชนิดอัตโนมัติ มายเกรชันแบบประกาศ และการรวมเข้ากับ NestJS แบบเนทีฟช่วยให้พัฒนา API ที่แข็งแรงได้อย่างรวดเร็ว ### เช็กลิสต์สำหรับการรวม NestJS + Prisma ที่ประสบความสำเร็จ - ✅ โมดูล Prisma แบบ Global เพื่อทำให้การฉีดง่ายขึ้น - ✅ สคีมา Prisma พร้อมความสัมพันธ์และดัชนีที่เหมาะสม - ✅ เซอร์วิสแบบกำหนดชนิดพร้อมการเลือกฟิลด์อย่างชัดเจน - ✅ ทรานแซกชันสำหรับการดำเนินงานแบบอะตอมิก - ✅ มิดเดิลแวร์สำหรับการตรวจสอบและ soft delete - ✅ แคชสำหรับคิวรีที่เรียกใช้บ่อย - ✅ การทดสอบเชิงรวมพร้อมฐานข้อมูลแยกอิสระ - ✅ การแบ่งหน้ามาตรฐานผ่าน extensions การผสมผสานนี้ใช้จุดแข็งของแต่ละเครื่องมือ ได้แก่ สถาปัตยกรรมโมดูลของ NestJS สำหรับโครงสร้าง และ Prisma สำหรับชั้นข้อมูลที่ปลอดภัยด้านชนิด ผลลัพธ์คือโค้ดที่บำรุงรักษาง่าย ทดสอบได้ และมีประสิทธิภาพสูง เหมาะสำหรับแอปพลิเคชันระดับองค์กร --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/node-nestjs/nestjs-prisma-modern-backend-stack