NestJS + Prisma: สแตกแบ็กเอนด์สมัยใหม่สำหรับ Node.js
คู่มือฉบับสมบูรณ์ในการสร้าง API แบ็กเอนด์สมัยใหม่ด้วย NestJS และ Prisma ครอบคลุมการตั้งค่า โมเดล เซอร์วิส ทรานแซกชัน และแนวปฏิบัติที่ดี

NestJS และ Prisma เป็นการผสมผสานที่ทรงพลังสำหรับการพัฒนาแบ็กเอนด์สมัยใหม่ NestJS มอบสถาปัตยกรรมแบบโมดูลและการฉีดดีเพนเดนซี ในขณะที่ Prisma มอบ ORM ที่ปลอดภัยด้านชนิดพร้อมประสบการณ์นักพัฒนาที่ยอดเยี่ยม สแตกนี้ช่วยให้สร้าง API ที่แข็งแรงและบำรุงรักษาง่ายได้
Prisma สร้างไคลเอนต์ TypeScript ที่มีชนิดข้อมูลโดยอัตโนมัติจากสคีมาฐานข้อมูล เมื่อรวมกับ NestJS และระบบโมดูล โค้ดจะอธิบายตัวเองได้และข้อผิดพลาดด้านชนิดจะถูกตรวจพบตั้งแต่ตอนคอมไพล์
การตั้งค่าโปรเจกต์เริ่มต้น
การรวม Prisma เข้ากับโปรเจกต์ NestJS ต้องผ่านขั้นตอนการตั้งค่าเพียงไม่กี่ขั้นตอน กระบวนการเป็นมาตรฐานและมีเอกสารครบถ้วน
# 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 สำหรับตัวแปรสภาพแวดล้อม
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()
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 {}การนำเข้าในโมดูลรากทำให้เซอร์วิสพร้อมใช้งานโดยอัตโนมัติ
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 ช่วยลดความซับซ้อนของสถาปัตยกรรม แต่ทำให้ดีเพนเดนซีกลายเป็นแบบซ่อน สำหรับแอปพลิเคชันขนาดเล็กก็ยอมรับได้ ในโปรเจกต์ขนาดใหญ่ การนำเข้าแบบชัดเจนช่วยให้ติดตามดีเพนเดนซีได้ง่ายขึ้น
การกำหนดสคีมา Prisma
ไฟล์ schema.prisma กำหนดโมเดลข้อมูล ความสัมพันธ์ และตัวเลือกของฐานข้อมูล Prisma ใช้ภาษานิยามสคีมาของตัวเอง (PSL)
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")
}หลังจากแก้ไขสคีมาแล้ว มายเกรชันจะนำการเปลี่ยนแปลงไปใช้กับฐานข้อมูล
# 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 การกำหนดชนิดอัตโนมัติช่วยให้โค้ดและฐานข้อมูลสอดคล้องกัน
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<User, 'password'>;
@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<SafeUser> {
// 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<SafeUser> {
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<SafeUser> {
// 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<void> {
await this.findOne(id);
// Deletion cascades to profile and posts
await this.prisma.user.delete({ where: { id } });
}
}การกำหนดชนิดของ Prisma ช่วยรับประกันว่าทุกคุณสมบัติที่ใช้มีอยู่ในสคีมา
พร้อมที่จะพิชิตการสัมภาษณ์ Node.js / NestJS แล้วหรือยังครับ?
ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ
การจัดการความสัมพันธ์ด้วย Prisma
Prisma ทำให้การจัดการความสัมพันธ์ที่ซับซ้อนเป็นเรื่องง่าย คิวรีแบบซ้อนช่วยให้โหลดข้อมูลที่เกี่ยวข้องได้ในคำขอครั้งเดียว
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 มีหลายวิธีในการรับประกันความเป็นอะตอมิกของการดำเนินงาน ทรานแซกชันแบบอินเทอร์แอคทีฟให้ความยืดหยุ่นมากที่สุด
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
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
import { Prisma, PrismaClient } from '@prisma/client';
// Extension for standardized pagination
export const paginationExtension = Prisma.defineExtension({
model: {
$allModels: {
async paginate<T, A>(
this: T,
args: Prisma.Exact<A, Prisma.Args<T, 'findMany'>> & {
page?: number;
limit?: number;
}
): Promise<{
data: Prisma.Result<T, A, 'findMany'>;
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' } });สำหรับคิวรีที่เรียกใช้บ่อย แคชจะช่วยปรับปรุงเวลาตอบสนองอย่างมีนัยสำคัญ
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
การทดสอบต้องใช้กลยุทธ์ฐานข้อมูลแบบแยกอิสระ การใช้ฐานข้อมูลเฉพาะสำหรับการทดสอบช่วยรับประกันความสามารถในการทำซ้ำ
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<Array<{ tablename: string }>>`
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();
}การทดสอบเชิงรวมใช้ตัวช่วยเหล่านี้เพื่อรับประกันสภาพแวดล้อมที่สะอาด
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 สำหรับชั้นข้อมูลที่ปลอดภัยด้านชนิด ผลลัพธ์คือโค้ดที่บำรุงรักษาง่าย ทดสอบได้ และมีประสิทธิภาพสูง เหมาะสำหรับแอปพลิเคชันระดับองค์กร
แท็ก
แชร์
บทความที่เกี่ยวข้อง

NestJS: สร้าง REST API ที่สมบูรณ์ตั้งแต่เริ่มต้น
คู่มือฉบับสมบูรณ์สำหรับการสร้าง REST API ระดับมืออาชีพด้วย NestJS ครอบคลุม Controller, Service, Module, การตรวจสอบข้อมูลด้วย class-validator และการจัดการข้อผิดพลาด

คำถามสัมภาษณ์ Backend Node.js: คู่มือฉบับสมบูรณ์ 2026
25 คำถามสัมภาษณ์ Backend Node.js ที่พบบ่อยที่สุด Event loop, async/await, streams, clustering และประสิทธิภาพอธิบายพร้อมคำตอบโดยละเอียด

Node.js Performance: Event Loop, Clustering และ Optimization ฉบับสมบูรณ์ 2026
คู่มือเชิงลึกเกี่ยวกับประสิทธิภาพ Node.js ครอบคลุมกลไก Event Loop, การทำ Clustering, Worker Threads, การจัดการหน่วยความจำ และเทคนิค Optimization สำหรับระบบ production พร้อมตัวอย่างโค้ดจริงที่นำไปใช้ได้ทันที