NestJS: สร้าง REST API ที่สมบูรณ์ตั้งแต่เริ่มต้น

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

คู่มือ NestJS สำหรับสร้าง REST API ที่สมบูรณ์

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 ที่จำเป็นที่ติดตั้งไว้แล้ว

src/main.tstypescript
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 จากโมดูลอื่น)

src/app.module.tstypescript
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

src/users/users.module.tstypescript
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 ทำให้โค้ดอ่านเข้าใจง่ายและอธิบายตัวเองได้

src/users/users.controller.tstypescript
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<User> {
    // 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<User> {
    // 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<User> {
    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<void> {
    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 จัดการให้

src/users/users.service.tstypescript
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<User> {
    // 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<User> {
    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<User> {
    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<void> {
    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<User | undefined> {
    return this.users.find(u => u.email === email);
  }
}

Exception NotFoundException สร้าง response HTTP 404 โดยอัตโนมัติพร้อมข้อความแจ้งข้อผิดพลาดที่จัดรูปแบบแล้ว

พร้อมที่จะพิชิตการสัมภาษณ์ Node.js / NestJS แล้วหรือยังครับ?

ฝึกฝนด้วยตัวจำลองแบบโต้ตอบ, flashcards และแบบทดสอบเทคนิคครับ

การตรวจสอบข้อมูลด้วย class-validator

DTO (Data Transfer Object) กำหนดโครงสร้างข้อมูลที่เข้ามา Decorator จาก class-validator ระบุกฎการตรวจสอบที่ถูกนำไปใช้โดยอัตโนมัติผ่าน ValidationPipe

src/users/dto/create-user.dto.tstypescript
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 โดยยังคงกฎการตรวจสอบไว้

src/users/dto/update-user.dto.tstypescript
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 แทนโครงสร้างข้อมูลที่เก็บไว้ในฐานข้อมูล

src/users/entities/user.entity.tstypescript
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 ข้อผิดพลาดให้สอดคล้องกันทั่วทั้งแอปพลิเคชัน

src/common/filters/http-exception.filter.tstypescript
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<Response>();
    const request = ctx.getRequest<Request>();
    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 ตัวที่สองรับประกันการครอบคลุมทุกกรณี

src/common/filters/all-exceptions.filter.tstypescript
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<Response>();
    const request = ctx.getRequest<Request>();

    // 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

src/main.ts (updated)typescript
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/schema.prismaprisma
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 ที่ใช้ซ้ำได้ทั่วทั้งแอปพลิเคชัน

src/prisma/prisma.service.tstypescript
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();
  }
}
src/prisma/prisma.module.tstypescript
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 เข้าถึงฐานข้อมูลจริง

src/users/users.service.ts (Prisma version)typescript
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<Omit<User, 'password'>> {
    // 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<Omit<User, 'password'>> {
    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<User, 'password'>;
  }

  async update(id: number, updateUserDto: UpdateUserDto): Promise<Omit<User, 'password'>> {
    // 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<User, 'password'>;
  }

  async remove(id: number): Promise<void> {
    await this.findOne(id);
    await this.prisma.user.delete({ where: { id } });
  }
}

Interceptor สำหรับการแปลง Response

Interceptor ช่วยแปลง response ให้เป็นรูปแบบเดียวกันทั่วทั้ง API Transform interceptor ทำให้รูปแบบ response ทั้งหมดเป็นมาตรฐานเดียวกัน

src/common/interceptors/transform.interceptor.tstypescript
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<T> {
  success: boolean;
  data: T;
  timestamp: string;
}

@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, ApiResponse<T>> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<ApiResponse<T>> {
    return next.handle().pipe(
      map(data => ({
        success: true,
        data,
        timestamp: new Date().toISOString(),
      })),
    );
  }
}

Logging interceptor ติดตาม request และเวลาที่ใช้ในการทำงานเพื่อตรวจสอบประสิทธิภาพ

src/common/interceptors/logging.interceptor.tstypescript
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<any> {
    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

แท็ก

#nestjs
#nodejs
#typescript
#rest api
#backend

แชร์

บทความที่เกี่ยวข้อง