Share on social media

nestjs-mongo สามารถดู video ของหัวข้อนี้ก่อนได้ ดู video

NestJS คืออะไร

Ref: https://nestjs.com/

NestJS คือ Framework ที่ออกแบบมาสำหรับการสร้าง web application ด้วย Node.js โดยจุดเด่นคือการใช้ภาษา JavaScript ยุคใหม่และสร้างบน “พื้นฐานของ TypeScript” ส่งผลให้การพัฒนา web application มีความรวดเร็วและสามารถไว้วางใจ (reliability) ได้มากขึ้นด้วยระบบตรวจสอบชนิดข้อมูล (strong typing) และเทคนิคการเขียนโปรแกรมเชิงวัตถุ (OOP)

ความโดดเด่นของ NestJS คือความสามารถในการผสมผสานแนวคิดจากหลายรูปแบบการเขียนโปรแกรม ไม่ว่าจะเป็นการเขียนโปรแกรมเชิงวัตถุ (OOP), การเขียนโปรแกรมแบบ Functional Programming (FP) และการเขียนโปรแกรมแบบ Reactive NestJS จึงเป็น Framework ที่ทั้งอเนกประสงค์และเหมาะแก่การนำมาพัฒนาด้าน server side ด้วยเช่นกัน

โดยตัว Framework ได้ออกแบบโดยคำนึงถึงการสร้างโครงสร้าง web application ที่ง่ายต่อการเขียน code เพื่อการทดสอบ (testable), สามารถ scalable, มีส่วนประกอบที่ไม่พึ่งพากัน (loosely coupled) และดูแลรักษาง่าย (maintainable) ความสามารถนี้เกิดจากความเป็น modular system ที่ส่งเสริมหลักการ separation of concerns และการ reuse นำ code กลับมาใช้ใหม่ได้จากคุณสมบัติของ modular นั่นเอง

โดยข้อดีของการใช้ NestJS สำหรับการสร้าง web application ฝั่งเซิร์ฟเวอร์ด้วย Node.js มีดังนี้

  • TypeScript Support NestJS สร้างขึ้นด้วย TypeScript มีระบบการตรวจสอบชนิดข้อมูลแบบ strong typing ซึ่งช่วยในการจับข้อผิดพลาดเรื่องประเภทข้อมูลได้รวดเร็ว
  • Modular Architecture Framework ส่งเสริมโครงสร้างแบบ Module ซึ่งช่วยให้นักพัฒนาสามารถจัดระเบียบ code เป็น module ต่างๆ ได้ง่าย รวมถึงนำกลับมาใช้ใหม่และแยกออกจากกันได้ง่ายขึ้น เป็นการลดความซับซ้อนในการบริหารจัดการ web application ขนาดใหญ่ และสนับสนุนการนำ code กลับมาใช้ใหม่ได้
  • NestJS มีระบบจัดการ dependency injection ในตัว ช่วยลดความยุ่งยากในการบริหารจัดการสิ่งที่พึ่งพากัน (dependencies) และเสริมประสิทธิภาพการทดสอบ ทำให้ง่ายต่อการเขียนโค้ดแยกส่วนและบำรุงรักษาได้ง่ายขึ้น
  • NestJS ถูกพัฒนามาจาก Express.js ทำให้ได้ประโยชน์จาก feature ในตัว Express ไปด้วย
  • NestJS สนับสนุนการเขียน code ที่สามารถทดสอบ (testable) ได้ ด้วยการใช้ dependency injection และ separation of concerns ทำให้เขียน test ในรูปแบบต่างๆได้ง่ายขึ้น
  • NestJS มีความสามารถในการปรับแต่งค่าต่างๆ ของ ORM อย่าง TypeORM, Prisma ได้ โดย ORM นี้จะช่วยให้นักพัฒนาทำงานกับ database โดยใช้รูปแบบอย่าง Active Record และ Data Mapper ได้ ส่งผลทำให้การจัดการ database ทำได้อย่างง่ายขึ้น

ด้วยข้อดีเหล่านี้ NestJS จึงเป็นตัวเลือกที่น่าสนใจสำหรับนักพัฒนาที่มองหาช่องทางในการสร้าง web application บน Node.js ที่สามารถ scale ได้ ง่ายต่อการดูแลรักษา และมีความเสถียรในการทำงานสูง ไม่ว่าจะเป็นสำหรับ product เล็กหรือใหญ่ก็สามารถพิจารณาใช้ NestJS ได้

องค์ประกอบหลักของ NestJS

นี่คือตัวอย่าง structure ของ NestJS

Terminal window
e-commerce-platform/
├── src/
├── modules/
├── products/
├── dto/
└── create-product.dto.ts
├── entities/
└── product.entity.ts
├── products.controller.ts
├── products.service.ts
└── products.module.ts
├── orders/
├── dto/
└── create-order.dto.ts
├── entities/
└── order.entity.ts
├── orders.controller.ts
├── orders.service.ts
└── orders.module.ts
└── users/
├── dto/
└── create-user.dto.ts
├── entities/
└── user.entity.ts
├── users.controller.ts
├── users.service.ts
└── users.module.ts
├── app.module.ts
└── main.ts
├── package.json
└── tsconfig.json

ส่วนประกอบหลักของ NestJS ที่สำคัญต่อการพัฒนาประกอบด้วย

  1. Controllers มีหน้าที่รับ HTTP request ที่เข้ามาและ response กลับไปยังฝั่ง client โดยตัว Controllers เปรียบเสมือนจุดเชื่อมต่อระหว่าง client และ server ทำหน้าที่ควบคุมการไหลของข้อมูลและการโต้ตอบระหว่างทั้งสองส่วน
  2. Providers สามารถเป็นได้ทั้ง services, repositories, factories ที่สามารถ inject เข้าไปใช้เป็น dependency ได้ Providers ถูกใช้เพื่อแยกส่วนของ logic) และ function การทำงานต่างๆ ทำให้ส่วนประกอบเหล่านั้นสามารถนำกลับมาใช้ใหม่ได้ โดย Providers จะถูกใส่เพิ่มผ่าน decorator ด้วยเครื่องหมาย @Injectable() ซึ่งเป็นการบอกว่า function เหล่านั้นสามารถถูกจัดการได้โดย dependency injection ของ NestJS
  3. Service คือ class ที่จัดการกับ business logic และการเข้าถึงข้อมูล โดยยึดตามหลักการ separation of concerns หน้าที่ของ service คือการดึงเอา logic การทำงานที่ซับซ้อนออกจาก controllers เพื่อให้ controllers ทำหน้าที่เฉพาะการจัดการด้าน Request หรือส่วนที่มีการรับเข้ามาเท่านั้น และในที่สุด service จะเป็นตัวส่งข้อมูล response ที่เหมาะสมกลับไปแทน
  4. Modules ถือเป็นหน่วยพื้นฐานในการสร้าง web application ด้วย NestJS โดยทำหน้าที่ช่วยแบ่งแยก code ออกเป็นส่วนตามการทำงานหรือ logic ที่เกี่ยวข้องกัน แต่ละ module จะห่อหุ้มไว้ด้วย providers, controllers และส่วนประกอบอื่นๆ ทำให้ application มีลักษณะแยกส่วนออกจากกันได้

MODULEstringnamestring[]importsstring[]controllersstring[]providersstring[]exportsCONTROLLERSERVICEPROVIDERdefinesusesimplementsprovidesimports

1. Controller

Controller ใน NestJS คือ class ที่ถูกเพิ่ม decorator @Controller() ซึ่งมีหน้าที่รับ HTTP requests ที่ส่งเข้ามา และส่งข้อมูลตอบกลับ (responses) ไปยังฝั่ง Client โดย Controllers จะกำหนด route เพื่อเชื่อม request ต่างๆ เข้ากับ function ที่เรียกว่า handler โดยที่ handler จะเป็นตัวประมวลผล request และส่งข้อมูล response ในรูปแบบที่เหมาะสมกลับไป

ตัวอย่างของ ProductsController ที่มีข้อมูล JSON จำลองสำหรับ request แบบ GET และ POST

products.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { ProductsService } from './products.service';
import { CreateProductDto } from './dto/create-product.dto';
@Controller('products')
export class ProductsController {
constructor(private readonly productsService: ProductsService) {}
@Get()
findAll() {
return this.productsService.findAll();
}
@Post()
create(@Body() createProductDto: CreateProductDto) {
return this.productsService.create(createProductDto);
}
}

โดย ถ้าทุกคนสังเกตเห็น เราจะเห็นตัวที่ชื่อ CreateProductDto เป็นตัวสำหรับกำกับ request body ที่ส่งเข้ามา สิ่งนั้นเราจะเรียกกันว่า Data Transfer Object (DTO)

ใน NestJS DTO คือ pattern ที่ใช้สำหรับถ่ายโอนข้อมูลระหว่างส่วนต่างๆ ของ web application โดย DTO ใช้ในการห่อหุ้มข้อมูล (encapsulate) เพื่อส่งจากฝั่ง client ไปยัง server ด้วยรูปแบบที่กำหนดไว้ใน DTO ได้ การใช้ DTO นั้นช่วยในการกำหนดโครงสร้างของข้อมูลที่เป็น input ซึ่งทำให้ชัดเจนว่าข้อมูลใดบ้างที่จำเป็นต้องมี และช่วยทำให้แน่ใจว่ามีเฉพาะข้อมูลที่ถูกต้องเท่านั้นที่ server จะรับเข้าไปได้

โดยทั่วไปใน DTO จะถูกสร้างในรูปแบบของ class ซึ่งเราสามารถใช้ประโยชน์จากการตรวจสอบชนิดข้อมูล (type checking) ของ TypeScript และการใช้ decorators จาก library อย่างเช่น class-validator เพื่อเพิ่ม rule การตรวจสอบความถูกต้อง (validation) เข้าไปได้ (เดี๋ยวเราจะมีแนะนำกันในหัวข้อนี้อีกที)

ตัวอย่าง code สำหรับ class CreateProductDto ที่ใช้สำหรับการกำหนดโครงสร้างและกฎการตรวจสอบสำหรับการสร้าง product ใหม่มีดังนี้

create-product.dto.ts
import { IsString, IsInt, Min, MaxLength } from 'class-validator';
export class CreateProductDto {
@IsString()
@MaxLength(100)
readonly name: string;
@IsString()
@MaxLength(500)
readonly description: string;
@IsInt()
@Min(1)
readonly price: number;
}

2. Provider

Provider ใน NestJS คือ group ที่ประกอบไปด้วย services, repositories, factories และ helpers โดย Providers จะเป็น class ที่มี decorator @Injectable() กำกับอยู่ ทำหน้าที่รับผิดชอบการจัดการ business logic, การจัดการข้อมูล (data management) และการทำงานอื่นๆ ที่สามารถถูกนำไป inject ให้กับ controllers เพื่อใช้งานได้

โดย Service ก็เป็น Provider ชนิดหนึ่งโดยทั่วไปจะประกอบด้วย method ต่างๆ ที่เกี่ยวข้องกับการจัดการหรือการทำงานของ feature ต่างๆ เช่นตาม ตัวอย่างของ ProductsService

products.service.ts
import { Injectable } from '@nestjs/common';
import { CreateProductDto } from './dto/create-product.dto';
@Injectable()
export class ProductsService {
private readonly products = [
{ id: 1, name: 'Product A', price: 100 },
{ id: 2, name: 'Product B', price: 150 },
];
findAll() {
return this.products;
}
create(createProductDto: CreateProductDto) {
const newProduct = { id: Date.now(), ...createProductDto };
this.products.push(newProduct);
return newProduct;
}
}

3. Module

Module ใน NestJS คือ class ที่ถูกเพิ่มด้วย @Module() โดย Module ทำหน้าที่ห่อหุ้ม providers, controllers และ modules อื่นๆ เพื่อสร้าง group ที่ส่วนประกอบต่างๆ มีความเกี่ยวข้องกันให้สามารถเรียกใช้หากันได้

ตัวอย่างของ ProductsModule ที่รวมส่วนของ controller และ service ไว้ด้วยกันมีดังนี้

products.module.ts
import { Module } from '@nestjs/common';
import { ProductsController } from './products.controller';
import { ProductsService } from './products.service';
@Module({
controllers: [ProductsController],
providers: [ProductsService],
})
export class ProductsModule {}

สุดท้ายเมื่อสร้างทั้งหมดมา เราจะมารวมกันไว้ที่ app.module.ts

ใน app.module.ts ของ NestJS คือ root module ที่เป็นจุดเริ่มต้นของระบบ module ทั้งหมดภายใน application เราจะใช้ไฟล์นี้ในการกำหนด module หลักของ application ด้วย decorator @Module() โดย module หลักนี้ทำหน้าที่รวบรวมและเชื่อมส่วนประกอบต่างๆ ไม่ว่าจะเป็น feature modules, middleware, providers (services) รวมถึง controllers เข้าด้วยกัน

ตัวอย่าง app.module.ts

app.module.ts
import { Module } from '@nestjs/common';
import { ProductsModule } from './modules/products/products.module';
import { OrdersModule } from './modules/orders/orders.module';
import { UsersModule } from './modules/users/users.module';
@Module({
imports: [ProductsModule, OrdersModule, UsersModule],
controllers: [],
providers: [],
})
export class AppModule {}

4. Schema และ Entity

ใน NestJS คำว่า Schema มักหมายถึง Mongoose schema ซึ่งใช้เมื่อมีการทำงานร่วมกับฐานข้อมูล MongoDB โดย Schema จะเป็นตัวกำหนดโครงสร้างของ documents ภายใน collection ของ MongoDB เช่น ชนิดของ field, ค่าเริ่มต้น, validators เป็นต้น โดย Schema เหล่านี้จะถูกนำไปใช้ในการสร้าง Mongoose Model ซึ่งทำหน้าที่เป็นเหมือนตัวเชื่อม (Interface) ในการสื่อสารกับฐานข้อมูล MongoDB เพื่อสร้าง, ค้นหา, อัปเดต หรือลบข้อมูล ของ database ได้

product.schema.ts
import * as mongoose from 'mongoose';
export const ProductSchema = new mongoose.Schema({
name: { type: String, required: true },
description: { type: String, required: true },
price: { type: Number, required: true },
});

ในขณะที่คำว่า Entity มักจะถูกใช้เมื่อทำงานกับฐานข้อมูล SQL ร่วมกับ TypeORM หรือ Sequelize โดย Entity จะเป็นตัวแทนของ table ในฐานข้อมูล ซึ่งถูกนิยามโดยใช้ class โดยในภาษา TypeScript แต่ละ instance ของ Entity ก็จะเปรียบได้กับหนึ่ง row ในตารางของ database นั่นเอง

product.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export class Product {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
description: string;
@Column()
price: number;
}

ทั้ง Schema และ Entity ทำหน้าที่เป็น data models ที่บอกถึงโครงสร้างของข้อมูล และวิธีการที่จะจัดเก็บและดึงข้อมูลออกจากฐานข้อมูล นับว่ามีความสำคัญมากต่อการรักษาความสมบูรณ์ของข้อมูล (data integrity) และช่วยให้การทำงานต่างๆ กับข้อมูลภายใน web application กับ database สามารถทำงานร่วมกันได้ง่ายขึ้นด้วย

และนี่ก็คือพื้นฐานขององค์ประกอบของ NestJS เพื่อให้เกิดความเข้าใจมากขึ้น เราจะมาลองเล่นผ่านตัวอย่างกัน

มาทำพื้นฐาน RestAPI เบื้องต้นกัน

Start project

เริ่มต้นทำการ start project ผ่าน command ของ NestJS ที่แนะนำใน https://docs.nestjs.com/first-steps

Terminal window
npm i -g @nestjs/cli
nest new project-name

เมื่อทำการ ลง project เรียบร้อย ให้เราทำการเข้า folder project มาแล้ว run ด้วย

Terminal window
npm start

และลองเปิดผ่าน localhost:3000 ดูหากเปิดมาได้เรียบร้อยแปลว่า เราลง project อย่างถูกต้องแล้ว

nest-basic-01.webp

ซึ่ง project เริ่มต้นของ NestJS นั้นจะมีโครงสร้างตามนี้

Terminal window
├── nest-cli.json
├── package-lock.json
├── package.json
├── src
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   └── main.ts
├── test
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── tsconfig.build.json
└── tsconfig.json

โดย project นั้นจะเริ่มต้นจาก folder src และเมื่อเปิดมาเราก็จะเจอ 3 อย่าง ตามที่เราอธิบายไปตอนแรกเลยคือ Module, Controller, Service และเมื่อลองเปิด Controller (app.controller.ts) มาก็จะเจอว่ามีการเรียกใช้ getHello() อยู่ใน Controller เป็น path เริ่มต้นมา

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}

เมื่อเราลองเปิด Service (app.service.ts) เราก็จะเจอ AppService ที่เป็นต้นทางของการเรียกใช้งานจาก Controller (ที่ทำการ init ให้ผ่าน Dependency Injection แล้วเรียบร้อย) โดยเป็นการ return text คำว่า Hello World! ออกมา

import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}

และท้ายที่สุดที่ Module (app.module.ts) ทำการรวมของทั้งหมดเข้าด้วยกันทั้งจาก Controller และ Service

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

และท้ายที่สุด Module ที่ Export (AppModule) ออกไปก็ถูกเรียกใช้จากไฟล์หลักเพื่อ start server ที่ main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();

และนี่ก็คือการทำงานของ NestJS ดังนั้น หากต้องการให้ Service ตัวใดสามารถถูกเรียกใช้งานออกมาได้ก็จะต้องทำการมารวมตัวกันไว้ที่ AppModule เพื่อให้ AppModule ทำการส่งต่อออกมาเปิดเป็น HTTP Server ออกมาได้นั่นเอง

ลองสร้าง GET / POST กัน

Ref: https://docs.nestjs.com/cli/usages

NestJS นอกเหนือจากมี CLI สำหรับสร้าง project แล้ว ยังมีอำนวยความสะดวกในการสร้าง service อยู่ด้วยนะ เพื่อให้เราไม่งงเวลาต้องวาง Structure ของ NestJS โดยการสร้างผ่าน CLI นั้นสามารถสร้างได้ทั้ง Controller, Service, Module โดยสามารถใช้ผ่านคำสั่งเหล่านี้ได้

Terminal window
# สร้าง Controller
nest generate controller <name>
# สร้าง Service
nest generate service <name>
# สร้าง Module
nest generate module <name>

เราจะลองยกตัวอย่างจากการสร้าง products ด้วยการทำ Mock API กัน โดยการลองใช้คำสั่งสร้าง products Module ขึ้นมาตามนี้

Terminal window
nest generate controller products
nest generate service products
nest generate module products

และเมื่อเราลอง run ขึ้นมาก็จะเจอว่าคำสั่งเหล่านี้พยายามไปสร้าง Controller, Service และ Module ไว้ใน folder products ขึ้นมา

nest-basic-02.webp

ดังนั้นเมื่อเราลองสำรวจ project structure อีกที ก็จะเจอว่ามี folder ของ products ขึ้นมาพร้อมกับ pattern มาตรฐาน Controller, Module, Service เป็นที่เรียบร้อย

Terminal window
├── src
│   ├── app.controller.spec.ts
│   ├── app.controller.ts
│   ├── app.module.ts
│   ├── app.service.ts
│   ├── main.ts
│   └── products
│   ├── products.controller.spec.ts
│   ├── products.controller.ts
│   ├── products.module.ts
│   ├── products.service.spec.ts
│   └── products.service.ts
├── tsconfig.build.json
└── tsconfig.json

เมื่อเราลองดูที่ app.module.ts อีกที ก็จะเจอว่ามีการเรียกใช้งานทั้ง Controller, Service และ Module เข้ามาใน AppModule หลักเป็นที่เรียบร้อย (จริงๆมันไม่จำเป็นต้องเอาเข้ามาขนาดนี้ก็ได้ หากเราไม่ได้มีความจำเป็นเรียกใช้ Service อะไร ซึ่งเดี๋ยวเราจะมีท่าที่ดีกว่าในการสร้างและจะทำการ import เฉพาะ module เข้ามาได้)

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ProductsController } from './products/products.controller';
import { ProductsService } from './products/products.service';
import { ProductsModule } from './products/products.module';
@Module({
imports: [ProductsModule],
controllers: [AppController, ProductsController],
providers: [AppService, ProductsService],
})
export class AppModule {}

และเมื่อเราไปดูใน 3 files products.controller.ts, products.module.ts, products.service.ts ก็จะมีโครงสร้างที่เตรียมเป็นพื้นฐานไว้เรียบร้อย เริ่มต้นเราจะเริ่มสร้างจาก products.service.ts กันก่อน ใน Service เราจะทำการสร้าง function สำหรับเตรียมเรียกใช้งานใน API โดยทำการสร้าง array products เป็น private สำหรับเป็น array ที่จำลองเสมือนว่าเก็บอยู่ใน database และ

  • findAll() = สำหรับดึง product ทั้งหมดออกมา
  • create(product) = สำหรับสร้าง product ออกมา

ก็จะได้ code Service หน้าตาประมาณนี้ออกมา

import { Injectable } from '@nestjs/common';
@Injectable()
export class ProductsService {
private readonly products = [
{ id: 1, name: 'Product 1', description: 'Description 1' },
{ id: 2, name: 'Product 2', description: 'Description 2' },
];
findAll() {
return this.products;
}
create(product) {
this.products.push(product);
return product;
}
}

มาที่ Controller products.controller.ts ทำการสร้าง HTTP Request 2 path คือ

  • GET /products = สำหรับดึง products ทั้งหมดออกมา
  • POST /products = สำหรับสร้าง products ใหม่และเพิ่มเข้าไปเพื่อเรียกดูจากฝั่ง GET ได้
import { Controller, Get, Post, Body } from '@nestjs/common';
import { ProductsService } from './products.service';
@Controller('products')
export class ProductsController {
constructor(private readonly productsService: ProductsService) {}
@Get()
getAllProducts() {
return this.productsService.findAll();
}
@Post()
addProduct(@Body() product: any) {
return this.productsService.create(product);
}
}

หลังจากนั้น Module ทำการผูกทุกอย่างรวมกัน โดยทำการเรียกใช้ Service (ที่มีการใช้ Injection เพื่อทำการ auto instance เข้าไปยัง productsService ที่อยู่ใน Controller) และ Controller มารวมไว้เป็น Module เดียวกัน

import { Module } from '@nestjs/common';
import { ProductsService } from './products.service';
import { ProductsController } from './products.controller';
@Module({
controllers: [ProductsController],
providers: [ProductsService],
})
export class ProductsModule {}

หลังจากนั้นเมื่อลองยิง request ก็จะสามารถได้ผลลัพธ์ออกมาตามนี้

nest-basic-03.gif

และนี่ก็คือตัวอย่างของการทำ HTTP Request จะเห็นว่า ไอเดียคือ

  1. สร้าง Service สำหรับใช้งานใน API
  2. สร้าง Controller เป็นตัวแทนของฝั่ง HTTP Request ที่รับมาจาก user (GET / POST)
  3. สร้าง Module สำหรับรวม Service และ Controller เข้าด้วยกัน

เพียงเท่านี้เราก็จะสามารถได้ API 1 module ขึ้นมาได้เรียบร้อย

CRUD กับ mongo

Setup project ใหม่อีกรอบ

Ref: https://docs.nestjs.com/techniques/mongodb

ทีนี้เราจะมาลองทำอะไรให้มันสมจริงยิ่งขึ้นเราจะลองทำ CRUD API จริงๆ ออกมาโดยเราจะทำการเพิ่ม set ของ API products ที่สามารถ

  • CREATE = สร้าง product ได้
  • READ = ดึงข้อมูล product, list product ออกมาได้
  • UPDATE = แก้ไข product ได้
  • DELETE = ลบ product ออกมาได้

โดย เพิ่มเติม เราจะมาลองต่อเข้ากับ Database mongoDB กันโดย NestJS เองก็ได้มี package สำหรับต่อ mongo ไว้เรียบร้อย สามารถลงได้จากคำสั่งนี้เลย

Terminal window
npm i @nestjs/mongoose mongoose

และทำการสร้าง mongodb โดยใช้ docker-compose.yml ในการสร้างขึ้นมาตามไฟล์นี้ (เป็น mongodb container เพียงตัวเดียว)

version: '3.8'
services:
mongo:
image: mongo
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
volumes:
mongo-data:
driver: local

หลังจากนั้นทำการ run docker ขึ้นมาโดยใช้คำสั่ง

docker-compose up -d

ก็จะสามารถ run mongo service จาก docker ขึ้นมาได้ เมื่อลอง docker ps ขึ้นมาและเจอว่า mongo service ขึ้นมาใน docker ได้ = run ถูกต้องแล้วเรียบร้อย

nest-basic-05.webp

ทีนี้ step ต่อมาเราจะเริ่มสร้างไฟล์สำหรับ service API กัน โดย NestJS เองได้คิดไว้อยู่แล้วว่า Pattern โดยทั่วไปเวลาที่เราทำ API นั้นจะเป็นการทำตาม Pattern ของ CRUD (Resource pattern) ดังนั้น NestJS จึงได้เตรียม CLI อีกตัวหนึ่งซึ่งจะทำการสร้าง CRUD ตัวอย่างขึ้นมาให้ทั้ง set ออกมาได้ โดยการใช้คำสั่งนี้ได้ (หากใครใช้ project เดิมลอง ให้ทำการลบตัวที่ import เพิ่มมาใน app.module.ts ก่อนใช้คำสั่ง และลบของที่อยู่ใน folder products ทั้งหมดออกก่อน)

Terminal window
nest g resource products

หลังจากใช้คำสั่งก็จะเจอโครงสร้างคล้ายๆกับเหมือนใช้คำสั่ง service, module และ controller รวดเดียวออกมาได้ และมีตัวแถมคือ dto เพิ่มมา

  • dto ที่เพิ่มมาจะเป็น dto ที่ผูกกับ body request ของ POST ที่ใช้สำหรับรับ body มาเพื่อสร้าง และ PUT ที่ใช้สำหรับรับ body มาเพื่อแก้ไข ได้เลย
  • ซึ่งจะเป็นตัวที่ช่วยกำกับ request ที่จะรับเข้ามาและแปลงเป็น object ตาม pattern ที่มีการระบุใน dto ไว้ได้
  • ส่วน entities จะเป็นตัวที่ใช้กับ ORM (ของฝั่งพวก Relational Database) ดังนั้นเราจะขอลบก่อนเนื่องจากเราไม่ได้ใช้ใน case นี้ แต่เดี๋ยวเราจะสร้าง schema มาเพื่อกำกับโครงสร้างของ Database แทน

ดังนั้น structure จะเหลือออกมาหน้าตาประมาณนี้ออกมา

Terminal window
.
── src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── main.ts
└── products
├── dto
├── create-product.dto.ts
└── update-product.dto.ts
├── products.controller.spec.ts
├── products.controller.ts
├── products.module.ts
├── products.service.spec.ts
└── products.service.ts

เมื่อลองดูในแต่ละไฟล์สังเกตว่าจะยังเป็นเพียง mock ของแต่ละตัวออกมา หากเราลอง run ใหม่และลองยิงออกมาก็จะเจอ response ตัวอย่างหน้าตาประมาณนี้ออกมาได้

nest-basic-04.webp

สุดท้ายหลังจากสร้าง resource มาได้เรียบร้อย ก่อนที่เราจะเริ่มทำ CRUD API จริงๆ เราจำเป็นต้อง config mongodb เข้า NestJS โดยเราจะทำการต่อ mongodb ผ่าน Mongoose เข้ามาใน config ของ NestJS ที่ app.module.ts โดยให้ AppModule ทำการ import module ของ Mongoose เข้ามา เพื่อให้ทุกที่ภายใน Application สามารถใช้งาน Mongoose module ให้เป็นตัวแทนของ Mongodb ได้

ที่ app.module.ts ก็จะมีหน้าตาประมาณนี้

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { ProductsModule } from './products/products.module';
@Module({
imports: [
MongooseModule.forRoot(`mongodb://localhost:27017`, {
user: 'root',
pass: 'example',
dbName: 'mikelopster',
}),
ProductsModule,
],
})
export class AppModule {}

เมื่อทำการ breakdown code มา

  • MongooseModule.forRoot() ถูกเรียกใช้ภายใน imports array ของ @Module decorator โดย method นี้จะเป็นการ initializes  Mongoose connection ไปยัง MongoDB database
  • โดยการ import MongooseModule.forRoot() เข้ามาใน root module (AppModule) ส่งผลทำให้ Mongoose สามารถเรียกใช้งานได้ผ่านทุก Module ใน Application

เมื่อ run project ด้วย npm run start:dev อีกรอบ และเจอว่าไม่มี Error อะไรขึ้นมา = ทุกอย่าง setup เรียบร้อย ตอนนี้ NestJS ได้ทำการต่อ mongo database ผ่าน mongoose

** สำหรับ Mongoose หากใครอยากรู้จักเพิ่มเติมสามารถไปอ่านหรือดูได้ผ่านหัวข้อนี้เลย https://blog.mikelopster.dev/mongo-basic/

เริ่มทำ CRUD API

Step ต่อมา เราจะทำ API ออกมาโดยยึดตาม Resource ที่ได้ทำการสร้างมาใน products.controller.ts โดย

  • GET /products = ดึง products ทั้งหมด
  • POST /products = สร้าง products
  • GET /products/:id = ดึง product ทั้งหมดออกมาราย id
  • PUT /products/:id = แก้ไข product ตาม id
  • DELETE /products/:id = ลบ product ตาม id

โดย step แรกขอเริ่มจากการปรับ Controller ก่อนโดยการเปลี่ยนจาก +id เป็น id แทน เนื่องจากคำสั่ง +id นั้น เป็นคำสั่งสำหรับการแปลง string เป็น int เนื่องจากใน ORM ที่ใช้กับ Relational Database หลายๆตัวจะใช้ id เป็นตัวเลข ดังนั้น คำสั่งเริ่มต้นของ NestJS จึงเลือกทำการ convert id ไปก่อน

  • ใน Controller จะมีการระบุ @Controller('products') ซึ่งเป็นการบอกว่า Controller ตัวนี้จะมี path เริ่มต้นเป็น /products ทั้งหมด
  • ดังนั้น HTTP method ที่อยู่ภายใน Controller นี้ก็จะเป็น method ที่ต่อท้ายจาก url ของ /products ไปอีกที (เช่น @Get() ก็จะเป็น path /products, @Get(':id') ก็จะเป็น path /products/:id เป็นต้น)

code products.controller.ts ก็จะมีหน้าตาเป็นแบบนี้

import {
Controller,
Get,
Post,
Body,
Param,
Delete,
Put,
} from '@nestjs/common';
import { ProductsService } from './products.service';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';
@Controller('products')
export class ProductsController {
constructor(private readonly productsService: ProductsService) {}
@Post()
create(@Body() createProductDto: CreateProductDto) {
return this.productsService.create(createProductDto);
}
@Get()
findAll() {
return this.productsService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.productsService.findOne(id);
}
@Put(':id')
update(@Param('id') id: string, @Body() updateProductDto: UpdateProductDto) {
return this.productsService.update(id, updateProductDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.productsService.delete(id);
}
}

ที่ dto เราจะทำการปรับเพื่อให้สามารถรับ request json 3 fields ที่หน้าตาประมาณนี้ออกมาได้

Terminal window
{
"name": "test name",
"description": "test description",
"price": 300
}

ดังนั้น ส่วนของ create-product.dto.ts และ update-product.dto.ts ก็จะมีหน้าตาเป็นแบบนี้ออกมาได้ (ทั้ง Create และ Update request เหมือนกัน เราจะใช้เพียง id ตรง url ในการแยก case เท่านั้น)

  • โดยสัญลักษณ์ ? เป็นการบอกว่าเป็น optional โดยจะสามารถส่งหรือไม่ส่งมาก็ได้
create-product.dto.ts
export class CreateProductDto {
readonly name: string;
readonly description?: string;
readonly price: number;
}
// update-product.dto.ts
export class UpdateProductDto {
readonly name?: string;
readonly description?: string;
readonly price?: number;
}

ทีนี้ส่วนสำหรับ Controller และ DTO ก็เรียบร้อย ต่อมากำหนด schema ของ Product ต่อ เพื่อเป็นต้นแบบของโครงสร้างข้อมูลใน collection ของ Product เพื่อใช้งานใน Service ที่ schemas/product.schema.ts โดยจาก dto ด้านบน เราจะทำการเก็บ field เหมือนกันลงใน database คือ

  • name เก็บชื่อ product เป็นประเภท string
  • description เก็บคำอธิบาย product เป็นประเภท string
  • price เก็บราคา product เป็นประเภท number

หน้าตาของ schema ก็จะออกมาเป็นประมาณนี้

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document } from 'mongoose';
export type ProductDocument = Product & Document;
@Schema()
export class Product {
@Prop({ required: true })
name: string;
@Prop()
description: string;
@Prop()
price: number;
}
export const ProductSchema = SchemaFactory.createForClass(Product);

กลับมาที่ products.service.ts เราจะทำการสร้าง function สำหรับ code CRUD ขึ้นมา โดย funtion ในนี้จะเป็น function ที่ถูกเรียกใช้จาก Controller และใช้ Schema เป็นตัวแทนสำหรับการ สร้าง / แก้ไข / อ่าน / ลบ ข้อมูล Product ออกมาได้ Service ที่รวม CRUD ไว้ก็จะมีคำสั่งประมาณนี้ (คำสั่งนี้ดูเพิ่มเติมใน Document ของ mongoose ได้ รวมถึงสามารถกลับไปดูในหัวข้อของ Mongoose ของช่อง Mikelopster ได้เช่นกัน)

import {
Injectable,
NotFoundException,
InternalServerErrorException,
} from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Product, ProductDocument } from './schemas/product.schema';
import { CreateProductDto } from './dto/create-product.dto';
import { UpdateProductDto } from './dto/update-product.dto';
@Injectable()
export class ProductsService {
constructor(
@InjectModel(Product.name) private productModel: Model<ProductDocument>,
) {}
async create(createProductDto: CreateProductDto): Promise<Product> {
const createdProduct = new this.productModel(createProductDto);
return createdProduct.save();
}
async findAll(): Promise<Product[]> {
return this.productModel.find().exec();
}
async findOne(id: string): Promise<Product> {
return this.productModel.findById(id).exec();
}
async update(
id: string,
updateProductDto: UpdateProductDto,
): Promise<Product> {
return this.productModel
.findByIdAndUpdate(id, updateProductDto, { new: true })
.exec();
}
async delete(id: string): Promise<{ message: string }> {
try {
const result = await this.productModel.findByIdAndDelete(id).exec();
if (!result) {
throw new NotFoundException(`Product with ID ${id} not found`);
}
return { message: 'Delete Successful' };
} catch (error) {
// Handle or transform the error as needed
throw new InternalServerErrorException(
'An error occurred during deletion',
);
}
}
}

สุดท้าย กลับมาที่จุดรวมพล products.module.ts ทำการเรียกใช้ Service, Controller ทั้งหมด รวมถึงเรียกใช้ MongooseModule.forFeature เพื่อทำการเชื่อมต่อ Mongoose กลับไปยัง MongoDB เพื่อบอกว่ามีการใช้ Schema Product เป็นตัวแทนในการสื่อสารกลับไปยัง product collection ใน mongo

  • MongooseModule.forFeature เป็น method ที่อยู่ภายใน package @nestjs/mongoose โดยจะใช้ในการ config MongooseModule และระบุว่า model หรือ schema ใดบ้างที่ควรจะ register ลงภายใต้ขอบเขตของ module ที่เรียกใช้งาน
  • method forFeature จะรับค่าเป็น Array ของ Object ซึ่งแต่ละ Object จะหมายถึง model หรือ schema ของ Mongoose ที่ต้องการ register เข้าไป โดยแต่ละ Object ควรจะมี properties ดังนี้
    • name ชื่อของ Model ที่จะถูกนำไปใช้ (Model ที่ถูก inject เข้าไป)
    • schema นิยามของ Schema Mongoose สำหรับ Model นั้นๆ

และนี่คือ code ของการใช้งานที่ products.module.ts ที่ทำการเรียกใช้ Product schema เพื่อทำการ register Model Product เข้าไป

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { ProductsService } from './products.service';
import { ProductsController } from './products.controller';
import { Product, ProductSchema } from './schemas/product.schema';
@Module({
imports: [
MongooseModule.forFeature([{ name: Product.name, schema: ProductSchema }]),
],
controllers: [ProductsController],
providers: [ProductsService],
})
export class ProductsModule {}

และนี่คือผลลัพธ์ที่เราทำมา เมื่อ run ด้วย npm run start:dev อีกรอบ (หรือจริงๆไม่ต้อง run ใหม่ก็ได้เนื่องจากคำสั่ง npm run start:dev ได้ทำการ watch file ไว้อยู่แล้ว) แล้วเราลองยิงตาม Path ของ Controller ดูก็จะได้ผลลัพธ์ API แบบฉบับของ CRUD ต่อเข้ากับ Mongo ออกมาได้

nest-basic-06.gif

เพิ่ม validation และ error

ทีนี้เพื่อให้ระบบเราออกมาสมบูรณ์ยิ่งขึ้นเราจำเป็นต้องทำ validation เหล่า request ที่ยิงเข้ามาด้วย เพื่อเป็นการตรวจสอบว่าข้อมูลที่ส่งเข้ามานั้นถูกประเภทหรือไม่ (name เป็น string จริงๆใช่ไหม, price เป็น number จริงๆใช่ไหม) เราจะเพิ่มเรื่องของการ validation เข้าไปพร้อมกับ Error message ที่เป็นการระบุว่าหาก field ไหนมีปัญหาให้ทำการ throw error ออกมาได้

สำหรับการ validate ลง package จากคำแนะนำของ NestJS https://docs.nestjs.com/techniques/validation เราสามารถใช้ 2 library นี้ในการทำ validation ได้ ให้ทำการลงเพิ่มที่ product เข้าไป

Terminal window
npm install class-validator class-transformer

ที่ dto/create-product.dto.ts และ dto/update-product.dto.ts ทำการปรับการใช้ schema จากแต่เดิม ที่มีการประกาศประเภทไว้เฉยๆ ทำการเรียกใช้การ validate ผ่าน pacakge class-validator เข้ามา

create-product.dto.ts
import { IsString, IsNumber, IsOptional } from 'class-validator';
export class CreateProductDto {
@IsString() // ตรวจสอบว่าเป็น string ไหม
readonly name: string;
@IsOptional()
@IsString()
readonly description?: string;
@IsNumber() // ตรวจสอบว่าเป็น integer ไหม
readonly price: number;
}
// update-product.dto.ts
import { IsString, IsNumber, IsOptional } from 'class-validator';
export class UpdateProductDto {
@IsOptional()
@IsString()
readonly name?: string;
@IsOptional()
@IsString()
readonly description?: string;
@IsOptional()
@IsNumber()
readonly price?: number;
}

และ เพื่อให้การ validation นั้นสามารถ handle error ได้จากทุกที่ (คือทุกที่เราจะทำการ handle error เรื่อง validation เหมือนกันหมด เพื่อให้ตอนใช้งาน ฝั่ง Frontend จะทราบได้ว่าการ handle validation นั้นสามารถทำด้วยวิธีเดียวกันได้เลย) ที่ main.ts ทำการเพิ่ม validate error เข้าไป

  • ValidationPipe นั้นเป็น pipe สำเร็จรูปที่ช่วยให้การกำหนด rule ในการตรวจสอบความถูกต้อง (validation) ของข้อมูลที่ส่งมาจากฝั่ง client สามารถทำได้ง่ายขึ้น โดย rule การตรวจสอบต่างๆ จะถูกกำหนดโดยใช้ decorators จาก class-validator (ตัวที่เรามีการประกาศไว้ใน dto ก่อนหน้า)
  • ด้วยการใช้ ValidationPipe ร่วมกับ config ต่างๆ (ตามที่เพิ่มใน code ด้านล่างนี้) ทำให้ NestJS สามารถมั่นใจได้ว่าข้อมูลภายนอกจะถูกตรวจสอบตาม rule ที่กำหนดไว้เสมอ รวมถึง properties ใดที่ไม่ได้อยู่ใน whitelist ก็จะถูกตัดออกโดยอัตโนมัติ วิธีนี้จะช่วยให้ข้อมูลภายใน web application มีความปลอดภัยได้ดีขึ้นด้วยเช่นกัน
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe, BadRequestException } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Set up global validation pipe
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // ตัด properties ของข้อมูลที่ส่งเข้ามาที่ไม่ได้นิยามไว้ใน dto ออกไป
forbidNonWhitelisted: true, // ตัวเลือกนี้จะทำงานคู่กับ whitelist โดยหากตั้งค่าเป็น true จะทำให้เกิด error ในกรณีนี้มี properties ใดที่ไม่ได้อยู่ใน whitelist ส่งเข้ามา
transform: true, // ตัวเลือกนี้ทำให้เกิดการแปลงชนิดข้อมูลอัตโนมัติ ในข้อมูลจากภายนอกให้ตรงกับชนิดที่นิยามไว้ใน DTO
exceptionFactory: (errors) => {
// ตัวเลือกนี้ทำให้สามารถกำหนดรูปแบบของ error response เมื่อการตรวจ validation ล้มเหลวได้
const messages = errors.map((error) => ({
field: error.property,
message: Object.values(error.constraints).join('. ') + '.',
}));
return new BadRequestException({ errors: messages });
},
}),
);
await app.listen(3000);
}
bootstrap();

และนี่คือผลลัพธ์ หลังจากที่เราเพิ่ม validation เข้าไปให้ลองทดสอบส่งข้อมูลผิดประเภทเข้าไปดูเช่น ส่ง price เป็น string เข้าไปแทน ก็จะเจอ error ออกมาได้

nest-basic-07.webp

เพิ่มใช้งานร่วมกับ order (เรียก product)

มาถึงอีก 1 use cases เพิ่มเติมนั่นคือการใช้งานร่วมกันหลาย Model ใน Mongo แน่นอน ระบบการทำงานทั่วไปนั้นเราไม่ได้มี database ที่มี collection หรือ table เพียงตัวเดียวอยู่แล้ว

ดังนั้น ใน case เราจะทำการเพิ่มการใช้งานอีก 2 API เข้ามาคือ

  • POST /orders = สำหรับสร้าง order อย่างง่ายโดยส่งมาเพียงแค่ product และ จำนวนของ product เท่านั้น โดยสำหรับ Product นั้น เราจะทำการรับ productId (Id จาก model Product) มาเป็นตัวเก็บ product เอาไว้ว่าสร้าง order โดยอ้างอิงกับ product ตัวไหนออกมา
  • GET /orders/:id = สำหรับดึง order ตาม orderId นั้นๆ และจะต้องสามารถ map ข้อมูล product กลับออกมาได้

เริ่มต้น ให้ทำการสร้าง CRUD API ตาม คำสั่งเดิมที่เคยใช้กับ products แต่เปลี่ยนมาเป็น orders แทน

nest g resource orders

หลังจากนั้น เราก็จะเจอ folder ของ orders ที่มี file structure เหมือนๆเดิมออกมา แต่เนื่องจากเคสนี้เราไม่ได้มีการทำเคสแก้ไขของ Order ดังนั้นทำการลบ update-order.dto.ts ไปเนื่องจากไม่ได้ใช้งานในเคสนี้ (รวมถึงจะขอลบไฟล์ที่เกี่ยวกับการ Test ทิ้งก่อน เพื่อให้ตัวอย่างเห็นภาพง่ายขึ้น) ดังนั้น file structure ก็จะมีหน้าตาแบบนี้ออกมา

├── src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── main.ts
├── orders (เพิ่มตัวนี้เข้ามา)
│ ├── dto
│ │ └── create-order.dto.ts
│ ├── orders.controller.ts
│ ├── orders.module.ts
│ ├── orders.service.ts
│ └── schemas
│ └── order.schema.ts
└── products (เหมือนเดิม)

หลังจากนั้นเริ่มเหมือนเดิม จาก Controller และ DTO ปรับให้ตรงตามโจทย์ของเราโดยจะเหลือเพียงแค่ @Post() และ @Get(':id') แค่ 2 API นี้ และจะทำการเรียกใช้ service ตามชื่อเดิมที่ Resource ได้ทำการเตรียมไว้ให้

ที่ orders.controller.ts ก็จะมี code หน้าตาแบบนี้

import { Body, Controller, Post, Get, Param } from '@nestjs/common';
import { OrdersService } from './orders.service';
import { CreateOrderDto } from './dto/create-order.dto';
@Controller('orders')
export class OrdersController {
constructor(private readonly ordersService: OrdersService) {}
@Post()
create(@Body() createOrderDto: CreateOrderDto) {
return this.ordersService.create(createOrderDto);
}
@Get(':id')
getOrderById(@Param('id') orderId: string) {
return this.ordersService.getOrderById(orderId);
}
}

ต่อมาที่ DTO ปรับให้ตรงตามโจทย์และเพิ่มการ validation เข้าไปที่ create-order.dto.ts โดยจะรับเพียง 2 fields คือ

  • productId เป็น string สำหรับการเก็บ Id ของ Product ไว้
  • number เป็น number สำหรับเก็บจำนวนของ Product ที่สั่งซื้อไว้

code DTO ก็จะหน้าตาประมาณนี้

import { IsMongoId, IsNotEmpty, IsNumber, Min } from 'class-validator';
export class CreateOrderDto {
@IsNotEmpty()
@IsMongoId()
readonly productId: string;
@IsNumber()
@Min(1)
readonly quantity: number = 1;
}

ฝั่ง Controller + DTO เสร็จเรียบร้อย ต่อมาเราจะทำการสร้าง schema เพื่อใช้สำหรับเป็นต้นแบบของ Order Schema ในการเก็บข้อมูลเข้า MongoDB

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import { Document, Types } from 'mongoose';
export type OrderDocument = Order & Document;
@Schema()
export class Order {
@Prop({ type: Types.ObjectId, ref: 'Product', required: true })
productId: Types.ObjectId; // Reference to Product model
@Prop({ required: true })
quantity: number;
}
export const OrderSchema = SchemaFactory.createForClass(Order);

Pattern จะคล้ายๆกันกับ Schema ของ Product เลยโดย

  • มี productId Reference กลับไปยัง ObjectId ของ Product
  • มี quantity ที่ไว้ใช้เก็บจำนวนของ product ที่มีการสร้าง order เข้ามา

ย้ายมาทำต่อที่ Service orders.service.ts ทำการเพิ่ม logic สำหรับการสร้าง order และดึง order เข้าไป

  • สำหรับ create นั้น เพื่อให้มั่นใจว่า product นั้นมี id อยู่จริงหรือไม่ เราจะนำ productId ไปค้นหากับ productService ออกมาก่อน (โดย productService นี้จะเป็นตัวที่โดน inject เข้ามาจาก orders.module.ts ผ่าน Product Module อีกที) และถ้าไม่ติดอะไรก็จะนำข้อมูลนั้นมาใช้ต่อเพื่อสร้างเป็น document ใหม่ใน order ได้
  • สำหรับ getOrderById เราจะทำการดึงข้อมูล order ร่วมกับ product โดยการใช้ populate('productId') เพื่อทำการ map field productId กลับไปยัง model Product เพื่อนำข้อมูล product ออกมาและทำการส่งออกผ่าน response กลับไป
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { Order, OrderDocument } from './schemas/order.schema';
import { CreateOrderDto } from './dto/create-order.dto';
import { ProductsService } from '../products/products.service';
@Injectable()
export class OrdersService {
constructor(
@InjectModel(Order.name) private orderModel: Model<OrderDocument>,
private productsService: ProductsService, // Inject the ProductsService
) {}
async create(createOrderDto: CreateOrderDto): Promise<Order> {
const productResult = await this.productsService.findOne(
createOrderDto.productId,
);
if (!productResult) {
throw new NotFoundException('product not found');
}
const result = new this.orderModel(createOrderDto);
return result.save();
}
async findOne(id: string): Promise<Order> {
const order = this.orderModel.findById(id).populate('productId').exec();
return order;
}
}

สุดท้ายรวมทั้งหมดที่ Module orders.module.ts โดยในรอบนี้เราจะทำการรับ ProductsModule เข้ามาด้วย

  • ProductsModule รับเข้ามาเพื่อทำการใช้คำสั่ง ProductsService สำหรับคำสั่งจัดการข้อมูลใน model Product ต่างๆเข้ามา (เช่น findOne ที่เรามีการใช้ในกรณีด้านบนใน service ของ Order)

ดังนั้นที่ code orders.modules.ts ก็จะมีหน้าตาประมาณนี้

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { OrdersService } from './orders.service';
import { OrdersController } from './orders.controller';
import { OrderSchema } from './schemas/order.schema';
import { ProductsModule } from '../products/products.module';
@Module({
imports: [
MongooseModule.forFeature([{ name: 'Order', schema: OrderSchema }]),
ProductsModule, // Import the ProductsModule
],
controllers: [OrdersController],
providers: [OrdersService],
})
export class OrdersModule {}

และที่ฝั่งของ products.module.ts เองก็จะต้อง export ทั้ง ProductsService ออกมาเช่นกัน

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { ProductsService } from './products.service';
import { ProductsController } from './products.controller';
import { Product, ProductSchema } from './schemas/product.schema';
@Module({
imports: [
MongooseModule.forFeature([{ name: Product.name, schema: ProductSchema }]),
],
controllers: [ProductsController],
providers: [ProductsService],
exports: [ProductsService],
})
export class ProductsModule {}

ก็จะได้ผลลัพธ์ตามโจทย์ที่เราต้องการออกมาได้

nest-basic-08.gif

เรียกใช้ config ผ่าน .env

Ref: https://docs.nestjs.com/techniques/configuration

สุดท้ายเพื่อให้เกิดความเป็นระเบียบเรียบร้อยของ config เราจะทำการย้าย mongo config มาไว้ที่ Environment Variable กัน โดยใน NestJS เองได้เตรียม package ไว้ให้แล้วนั่นก็คือ @nestjs/config

Terminal window
npm i @nestjs/config

ที่ .env ให้ทำการเพิ่ม config จากแต่เดิมที่เราเคยมีการ Fix ไว้ตอนแรกเข้ามา

MONGODB_URI=mongodb://localhost:27017
MONGODB_USER=root
MONGODB_PASS=example
MONGODB_DATABASE=mikelopster

สุดท้ายที่ app.module.ts ให้ทำการเรียกใช้ env ผ่าน ConfigModule, ConfigService ใน @nestjs/config ออกมา

  • ConfigModule import จาก @nestjs/config ใช้สำหรับการ handle environment variables จาก .env
  • ConfigService ทำหน้าที่ในการดึงค่า environment variable โดยใช้ method get ตัวอย่างเช่น configService.get<string>('MONGODB_URI') สำหรับดึงค่าจาก MONGODB_URI ใน .env
  • ใน MongooseModule.forRootAsync ตัวเลือก inject ระบุว่าเราต้องการให้ ConfigService ถูก inject ให้กับ function useFactory เพื่อให้สามารถใช้งาน configService ใน useFactory ได้

code ของ app.module.ts ก็จะมีหน้าตาประมาณนี้ และหากผลลัพธ์ออกมาเหมือนเดิมได้ เท่ากับว่าเราได้ config ทุกอย่างไว้อย่างถูกต้องแล้วเรียบร้อย ก็จะสามารถเรียกใช้ผ่าน .env แทนการ hard code ออกมาได้

import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ProductsModule } from './products/products.module';
import { OrdersModule } from './orders/orders.module';
@Module({
imports: [
ConfigModule.forRoot(), // Initialize ConfigModule
MongooseModule.forRootAsync({
imports: [ConfigModule], // Make ConfigModule available
useFactory: async (configService: ConfigService) => ({
uri: configService.get<string>('MONGODB_URI'),
user: configService.get<string>('MONGODB_USER'),
pass: configService.get<string>('MONGODB_PASS'),
dbName: configService.get<string>('MONGODB_DATABASE'),
}),
inject: [ConfigService], // Inject ConfigService to use it in useFactory
}),
ProductsModule,
OrdersModule,
],
})
export class AppModule {}

สรุป

และนี่ก็คือ NestJS CRUD กับ MongoDB ซึ่งถือเป็นท่ามาตรฐานทั่วไปในการทำ RestAPI จริงๆ ยังมี Use case อื่นๆที่ NestJS ยังคงสามารถทำได้อยู่เช่น

  • GraphQL APIs NestJS support ในการสร้าง GraphQL API ด้วย @nestjs/graphql
  • WebSockets สามารถใช้ NestJS ในการสร้าง web application แบบ realtime ได้ด้วย WebSockets โดยใช้ @nestjs/websockets
  • Microservices NestJS เหมาะในการสร้าง microservices ด้วยการรองรับรูปแบบการสื่อสารที่หลากหลาย protocal ที่ติดตั้งมาพร้อมกับตัว framework ไว้อยู่แล้ว
  • Authentication and Authorization NestJS มีระบบสำหรับติดตั้ง mechanisms การยืนยันตัวตนและการอนุญาตเข้าใช้งานไว้แล้ว (ใช้งานร่วมกับ passport ในการทำ Authentication ได้)
  • CRON Jobs สามารถใช้งานเพื่อ run task ตามกำหนดเวลา (scheduled tasks) และ CRON jobs ได้
  • Event-Driven Architecture NestJS รองรับ architecture แบบ event-driven ด้วยการรวมเข้ากับ message brokers อย่าง RabbitMQ ได้

เป็นต้น ทุกคนสามารถเข้าไปอ่านเพิ่มเติมได้ที่ https://docs.nestjs.com/techniques/configuration หวังว่าบทความนี้จะเป็นส่วนหนึ่งให้ทุกคนมาสนใจ NestJS กันนะครับ

Related Post

Share on social media