# NestJS: สร้าง REST API ที่สมบูรณ์ตั้งแต่เริ่มต้น > คู่มือฉบับสมบูรณ์สำหรับการสร้าง REST API ระดับมืออาชีพด้วย NestJS ครอบคลุม Controller, Service, Module, การตรวจสอบข้อมูลด้วย class-validator และการจัดการข้อผิดพลาด - Published: 2026-01-13 - Updated: 2026-04-07 - Author: SharpSkill - Tags: nestjs, nodejs, typescript, rest api, backend - Reading time: 15 min --- NestJS ได้ก้าวขึ้นเป็น framework Node.js อันดับหนึ่งสำหรับการสร้างแอปพลิเคชันฝั่ง server ที่สามารถขยายขนาดได้และดูแลรักษาง่าย ได้รับแรงบันดาลใจจาก Angular โดยนำเสนอสถาปัตยกรรมแบบโมดูล, dependency injection และการรองรับ TypeScript แบบ native การสร้าง REST API ระดับมืออาชีพจึงมีโครงสร้างที่ชัดเจนและคาดเดาได้ > **ทำไมต้องเลือก NestJS ในปี 2026** > > NestJS 11 นำเสนอการปรับปรุงประสิทธิภาพอย่างมีนัยสำคัญ, รองรับ native signal สำหรับ graceful shutdown และการเชื่อมต่อที่ง่ายขึ้นกับ ORM สมัยใหม่อย่าง Prisma และ Drizzle Framework ยังคงความเข้ากันได้ย้อนหลังกับเวอร์ชันก่อนหน้า ## การติดตั้งและการตั้งค่าโปรเจกต์ NestJS CLI สร้างโปรเจกต์ที่สมบูรณ์พร้อมโครงสร้างที่พร้อมใช้งานจริง TypeScript, ESLint และ unit test ถูกตั้งค่าโดยอัตโนมัติ ```bash # terminal # Global installation of the NestJS CLI npm install -g @nestjs/cli # Create a new project nest new my-api # Navigate to the project cd my-api # Start in development mode with hot-reload npm run start:dev ``` คำสั่งนี้สร้างโปรเจกต์พร้อมโครงสร้างไฟล์ที่เป็นระเบียบและ dependency ที่จำเป็นที่ติดตั้งไว้แล้ว ```typescript // src/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; async function bootstrap() { // Create the NestJS application const app = await NestFactory.create(AppModule); // Enable global validation app.useGlobalPipes(new ValidationPipe({ whitelist: true, // Removes non-decorated properties forbidNonWhitelisted: true, // Rejects requests with unknown properties transform: true, // Transforms payloads to DTO instances })); // Configure global prefix for all routes app.setGlobalPrefix('api'); await app.listen(3000); } bootstrap(); ``` การตั้งค่านี้เปิดใช้งานการตรวจสอบ request โดยอัตโนมัติและเพิ่ม prefix `/api` ให้กับทุก route ## ทำความเข้าใจสถาปัตยกรรมแบบโมดูล NestJS จัดระเบียบโค้ดเป็นโมดูล แต่ละโมดูลห่อหุ้มโดเมนการทำงานเฉพาะ โมดูลประกาศ controller (จุดเชื่อมต่อ HTTP), provider (service ธุรกิจ) และ import (dependency จากโมดูลอื่น) ```typescript // src/app.module.ts import { Module } from '@nestjs/common'; import { UsersModule } from './users/users.module'; import { ProductsModule } from './products/products.module'; import { AuthModule } from './auth/auth.module'; // The root module imports all application modules @Module({ imports: [ UsersModule, // User management ProductsModule, // Product catalog AuthModule, // Authentication ], }) export class AppModule {} ``` แต่ละโมดูลธุรกิจตามโครงสร้างเดียวกัน: ไฟล์ module, controller และ service ```typescript // src/users/users.module.ts import { Module } from '@nestjs/common'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; @Module({ // Controllers handle HTTP requests controllers: [UsersController], // Providers are injectable throughout the module providers: [UsersService], // Exports make providers available to other modules exports: [UsersService], }) export class UsersModule {} ``` การ export `UsersService` ทำให้โมดูลอื่นสามารถ import `UsersModule` และใช้งาน service นี้ผ่าน dependency injection ## สร้าง Controller CRUD ที่สมบูรณ์ Controller กำหนด route HTTP และมอบหมาย logic ธุรกิจให้ service Decorator ของ NestJS ทำให้โค้ดอ่านเข้าใจง่ายและอธิบายตัวเองได้ ```typescript // src/users/users.controller.ts import { Controller, Get, Post, Put, Delete, Body, Param, Query, HttpCode, HttpStatus, ParseIntPipe, } from '@nestjs/common'; import { UsersService } from './users.service'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from './entities/user.entity'; // Route prefix: /api/users @Controller('users') export class UsersController { // Inject service via constructor constructor(private readonly usersService: UsersService) {} // POST /api/users - Create a user @Post() @HttpCode(HttpStatus.CREATED) async create(@Body() createUserDto: CreateUserDto): Promise { // The DTO is automatically validated before reaching here return this.usersService.create(createUserDto); } // GET /api/users - List with pagination @Get() async findAll( @Query('page', new ParseIntPipe({ optional: true })) page: number = 1, @Query('limit', new ParseIntPipe({ optional: true })) limit: number = 10, ): Promise<{ data: User[]; total: number }> { return this.usersService.findAll(page, limit); } // GET /api/users/:id - Retrieve by ID @Get(':id') async findOne(@Param('id', ParseIntPipe) id: number): Promise { // ParseIntPipe automatically converts and validates the parameter return this.usersService.findOne(id); } // PUT /api/users/:id - Full update @Put(':id') async update( @Param('id', ParseIntPipe) id: number, @Body() updateUserDto: UpdateUserDto, ): Promise { return this.usersService.update(id, updateUserDto); } // DELETE /api/users/:id - Deletion @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) async remove(@Param('id', ParseIntPipe) id: number): Promise { await this.usersService.remove(id); } } ``` Pipe อย่าง `ParseIntPipe` รับประกันการแปลงและตรวจสอบพารามิเตอร์โดยอัตโนมัติ หากล้มเหลว ระบบจะส่งคืนข้อผิดพลาด 400 โดยไม่ต้องจัดการเอง > **Pipe ตรวจสอบที่มีให้ใช้งาน** > > NestJS มี pipe ให้ใช้งานหลายตัว: `ParseIntPipe`, `ParseBoolPipe`, `ParseArrayPipe`, `ParseUUIDPipe` แต่ละตัวทำหน้าที่ตรวจสอบและแปลงข้อมูลที่เข้ามาก่อนที่ handler จะทำงาน ## พัฒนา Business Service Service ห่อหุ้ม logic ธุรกิจและการเข้าถึงข้อมูล ทำเครื่องหมายด้วย `@Injectable()` เพื่อให้ dependency injection container ของ NestJS จัดการให้ ```typescript // src/users/users.service.ts import { Injectable, NotFoundException } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; import { UpdateUserDto } from './dto/update-user.dto'; import { User } from './entities/user.entity'; @Injectable() export class UsersService { // In-memory database simulation private users: User[] = []; private idCounter = 1; async create(createUserDto: CreateUserDto): Promise { // Create the user entity const user: User = { id: this.idCounter++, ...createUserDto, createdAt: new Date(), updatedAt: new Date(), }; this.users.push(user); return user; } async findAll(page: number, limit: number): Promise<{ data: User[]; total: number }> { // Calculate pagination const start = (page - 1) * limit; const end = start + limit; return { data: this.users.slice(start, end), total: this.users.length, }; } async findOne(id: number): Promise { const user = this.users.find(u => u.id === id); // Throw exception if user doesn't exist if (!user) { throw new NotFoundException(`User with ID ${id} not found`); } return user; } async update(id: number, updateUserDto: UpdateUserDto): Promise { const user = await this.findOne(id); // Merge existing data with updates Object.assign(user, updateUserDto, { updatedAt: new Date() }); return user; } async remove(id: number): Promise { const index = this.users.findIndex(u => u.id === id); if (index === -1) { throw new NotFoundException(`User with ID ${id} not found`); } this.users.splice(index, 1); } // Utility method for other services async findByEmail(email: string): Promise { return this.users.find(u => u.email === email); } } ``` Exception `NotFoundException` สร้าง response HTTP 404 โดยอัตโนมัติพร้อมข้อความแจ้งข้อผิดพลาดที่จัดรูปแบบแล้ว ## การตรวจสอบข้อมูลด้วย class-validator DTO (Data Transfer Object) กำหนดโครงสร้างข้อมูลที่เข้ามา Decorator จาก `class-validator` ระบุกฎการตรวจสอบที่ถูกนำไปใช้โดยอัตโนมัติผ่าน `ValidationPipe` ```typescript // src/users/dto/create-user.dto.ts import { IsEmail, IsNotEmpty, IsString, MinLength, MaxLength, IsOptional, Matches, } from 'class-validator'; export class CreateUserDto { @IsNotEmpty({ message: 'Email is required' }) @IsEmail({}, { message: 'Invalid email format' }) email: string; @IsNotEmpty({ message: 'Password is required' }) @MinLength(8, { message: 'Password must be at least 8 characters' }) @MaxLength(50, { message: 'Password cannot exceed 50 characters' }) @Matches( /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, { message: 'Password must contain at least one uppercase, one lowercase, and one number' } ) password: string; @IsNotEmpty({ message: 'First name is required' }) @IsString() @MinLength(2) @MaxLength(50) firstName: string; @IsNotEmpty({ message: 'Last name is required' }) @IsString() @MinLength(2) @MaxLength(50) lastName: string; @IsOptional() @IsString() @MaxLength(20) phone?: string; } ``` สำหรับการอัปเดตบางส่วน `PartialType` ทำให้ทุกฟิลด์เป็น optional โดยยังคงกฎการตรวจสอบไว้ ```typescript // src/users/dto/update-user.dto.ts import { PartialType, OmitType } from '@nestjs/mapped-types'; import { CreateUserDto } from './create-user.dto'; // All fields from CreateUserDto become optional // Password is excluded from standard updates export class UpdateUserDto extends PartialType( OmitType(CreateUserDto, ['password'] as const) ) {} ``` Entity แทนโครงสร้างข้อมูลที่เก็บไว้ในฐานข้อมูล ```typescript // src/users/entities/user.entity.ts export class User { id: number; email: string; password: string; firstName: string; lastName: string; phone?: string; createdAt: Date; updatedAt: Date; } ``` ## การจัดการข้อผิดพลาดแบบรวมศูนย์ NestJS มี HTTP exception ให้ใช้งานพร้อมใช้ Global exception filter ช่วยปรับแต่งรูปแบบ response ข้อผิดพลาดให้สอดคล้องกันทั่วทั้งแอปพลิเคชัน ```typescript // src/common/filters/http-exception.filter.ts import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, } from '@nestjs/common'; import { Request, Response } from 'express'; // Catches all HttpExceptions @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { catch(exception: HttpException, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); const status = exception.getStatus(); // Retrieve error message (can be string or object) const exceptionResponse = exception.getResponse(); const message = typeof exceptionResponse === 'string' ? exceptionResponse : (exceptionResponse as any).message; // Standardized response format response.status(status).json({ success: false, statusCode: status, timestamp: new Date().toISOString(), path: request.url, method: request.method, message: message, }); } } ``` สำหรับการดักจับข้อผิดพลาดที่ไม่ใช่ HTTP (ข้อผิดพลาดระบบ, exception ที่ยังไม่ถูกจัดการ) filter ตัวที่สองรับประกันการครอบคลุมทุกกรณี ```typescript // src/common/filters/all-exceptions.filter.ts import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus, } from '@nestjs/common'; import { Request, Response } from 'express'; // Catches ALL exceptions (including system errors) @Catch() export class AllExceptionsFilter implements ExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); // Determine HTTP code and message const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; const message = exception instanceof HttpException ? exception.message : 'Internal server error'; // Log error for debugging console.error('Exception caught:', exception); response.status(status).json({ success: false, statusCode: status, timestamp: new Date().toISOString(), path: request.url, message: message, }); } } ``` ลงทะเบียน filter แบบทั่วทั้งแอปพลิเคชันในไฟล์ bootstrap ```typescript // src/main.ts (updated) import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; import { HttpExceptionFilter } from './common/filters/http-exception.filter'; import { AllExceptionsFilter } from './common/filters/all-exceptions.filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); // Global validation app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true, })); // Global exception filters app.useGlobalFilters( new AllExceptionsFilter(), new HttpExceptionFilter(), ); app.setGlobalPrefix('api'); await app.listen(3000); } bootstrap(); ``` > **ลำดับของ Filter มีความสำคัญ** > > ลำดับการลงทะเบียน filter กำหนดลำดับการทำงาน Filter ที่ลงทะเบียนก่อนจะทำงานทีหลังสุด `AllExceptionsFilter` ต้องลงทะเบียนก่อน `HttpExceptionFilter` เพื่อทำหน้าที่เป็น fallback สำหรับ exception ที่ยังไม่ถูกจัดการ ## การเชื่อมต่อกับ Prisma ORM Prisma ทำให้การทำงานกับฐานข้อมูลง่ายขึ้นผ่าน typed client ที่สร้างขึ้นโดยอัตโนมัติจาก schema ต่อไปนี้คือการเชื่อมต่อทั้งหมดกับ NestJS ```bash # terminal # Install Prisma npm install prisma @prisma/client # Initialize Prisma with PostgreSQL npx prisma init --datasource-provider postgresql ``` การกำหนด data schema ระบุ model และความสัมพันธ์ ```prisma // prisma/schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) email String @unique password String firstName String @map("first_name") lastName String @map("last_name") phone String? createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relations posts Post[] @@map("users") } model Post { id Int @id @default(autoincrement()) title String content String published Boolean @default(false) authorId Int @map("author_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") // Relation to User author User @relation(fields: [authorId], references: [id]) @@map("posts") } ``` สร้าง Prisma module ที่ใช้ซ้ำได้ทั่วทั้งแอปพลิเคชัน ```typescript // src/prisma/prisma.service.ts import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { PrismaClient } from '@prisma/client'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { // Automatic connection on module startup async onModuleInit() { await this.$connect(); } // Clean disconnection on application shutdown async onModuleDestroy() { await this.$disconnect(); } } ``` ```typescript // src/prisma/prisma.module.ts import { Global, Module } from '@nestjs/common'; import { PrismaService } from './prisma.service'; // @Global makes the service available throughout the application @Global() @Module({ providers: [PrismaService], exports: [PrismaService], }) export class PrismaModule {} ``` Users Service ที่อัปเดตแล้วโดยใช้ Prisma เข้าถึงฐานข้อมูลจริง ```typescript // src/users/users.service.ts (Prisma version) import { Injectable, NotFoundException, ConflictException } 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 } from '@prisma/client'; import * as bcrypt from 'bcrypt'; @Injectable() export class UsersService { constructor(private readonly prisma: PrismaService) {} 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'); } // Hash the password const hashedPassword = await bcrypt.hash(createUserDto.password, 10); // Create the user const user = await this.prisma.user.create({ data: { ...createUserDto, password: hashedPassword, }, }); // Exclude password from response const { password, ...result } = user; return result; } async findAll(page: number, limit: number): Promise<{ data: User[]; total: number }> { // Parallel execution of count and paginated query const [data, total] = await Promise.all([ this.prisma.user.findMany({ skip: (page - 1) * limit, take: limit, orderBy: { createdAt: 'desc' }, select: { id: true, email: true, firstName: true, lastName: true, phone: true, createdAt: true, updatedAt: true, }, }), this.prisma.user.count(), ]); return { data: data as User[], total }; } async findOne(id: number): Promise> { const user = await this.prisma.user.findUnique({ where: { id }, select: { id: true, email: true, firstName: true, lastName: true, phone: true, createdAt: true, updatedAt: true, }, }); if (!user) { throw new NotFoundException(`User with ID ${id} not found`); } return user as Omit; } async update(id: number, updateUserDto: UpdateUserDto): Promise> { // Check existence await this.findOne(id); const user = await this.prisma.user.update({ where: { id }, data: updateUserDto, select: { id: true, email: true, firstName: true, lastName: true, phone: true, createdAt: true, updatedAt: true, }, }); return user as Omit; } async remove(id: number): Promise { await this.findOne(id); await this.prisma.user.delete({ where: { id } }); } } ``` ## Interceptor สำหรับการแปลง Response Interceptor ช่วยแปลง response ให้เป็นรูปแบบเดียวกันทั่วทั้ง API Transform interceptor ทำให้รูปแบบ response ทั้งหมดเป็นมาตรฐานเดียวกัน ```typescript // src/common/interceptors/transform.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; // Interface for standardized response format export interface ApiResponse { success: boolean; data: T; timestamp: string; } @Injectable() export class TransformInterceptor implements NestInterceptor> { intercept(context: ExecutionContext, next: CallHandler): Observable> { return next.handle().pipe( map(data => ({ success: true, data, timestamp: new Date().toISOString(), })), ); } } ``` Logging interceptor ติดตาม request และเวลาที่ใช้ในการทำงานเพื่อตรวจสอบประสิทธิภาพ ```typescript // src/common/interceptors/logging.interceptor.ts import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { tap } from 'rxjs/operators'; @Injectable() export class LoggingInterceptor implements NestInterceptor { private readonly logger = new Logger(LoggingInterceptor.name); intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); const { method, url } = request; const now = Date.now(); return next.handle().pipe( tap(() => { const response = context.switchToHttp().getResponse(); const { statusCode } = response; const duration = Date.now() - now; // Structured log format this.logger.log( `${method} ${url} ${statusCode} - ${duration}ms` ); }), ); } } ``` ## สรุป NestJS มอบสถาปัตยกรรมที่แข็งแกร่งและขยายขนาดได้สำหรับการสร้าง REST API ระดับมืออาชีพ การผสมผสานของ TypeScript, dependency injection และ decorator ที่มีประสิทธิภาพช่วยให้สร้างแอปพลิเคชันที่ดูแลรักษาง่ายและทดสอบได้ ### Checklist สำหรับ API NestJS ที่มีคุณภาพ - โครงสร้างแบบ module พร้อมการแบ่งหน้าที่รับผิดชอบชัดเจน - DTO พร้อมการตรวจสอบ class-validator สำหรับทุกข้อมูลที่เข้ามา - Service เฉพาะสำหรับ logic ธุรกิจ - การจัดการข้อผิดพลาดแบบรวมศูนย์ด้วย exception filter - Interceptor สำหรับการแปลงและ logging - เชื่อมต่อ Prisma สำหรับการเข้าถึงฐานข้อมูลแบบมีชนิดข้อมูล - ValidationPipe แบบทั่วทั้งแอปพลิเคชันพร้อมเปิดใช้ whitelist - API prefix ที่สอดคล้องกันบนทุก route จุดแข็งของ NestJS อยู่ที่โครงสร้างที่ชัดเจนซึ่งนำทางไปสู่ best practice โดยธรรมชาติ Pattern ที่พิสูจน์แล้วอย่าง dependency injection และการแบ่งชั้น layer สร้างโค้ดที่ทดสอบได้และพัฒนาต่อได้ พร้อมสำหรับแอปพลิเคชันระดับ enterprise --- 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-building-complete-rest-api