NestJS และ Mongo
/ 20 min read
สามารถดู 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
ส่วนประกอบหลักของ NestJS ที่สำคัญต่อการพัฒนาประกอบด้วย
- Controllers มีหน้าที่รับ HTTP request ที่เข้ามาและ response กลับไปยังฝั่ง client โดยตัว Controllers เปรียบเสมือนจุดเชื่อมต่อระหว่าง client และ server ทำหน้าที่ควบคุมการไหลของข้อมูลและการโต้ตอบระหว่างทั้งสองส่วน
- Providers สามารถเป็นได้ทั้ง services, repositories, factories ที่สามารถ inject เข้าไปใช้เป็น dependency ได้ Providers ถูกใช้เพื่อแยกส่วนของ logic) และ function การทำงานต่างๆ ทำให้ส่วนประกอบเหล่านั้นสามารถนำกลับมาใช้ใหม่ได้ โดย Providers จะถูกใส่เพิ่มผ่าน decorator ด้วยเครื่องหมาย
@Injectable()
ซึ่งเป็นการบอกว่า function เหล่านั้นสามารถถูกจัดการได้โดย dependency injection ของ NestJS - Service คือ class ที่จัดการกับ business logic และการเข้าถึงข้อมูล โดยยึดตามหลักการ separation of concerns หน้าที่ของ service คือการดึงเอา logic การทำงานที่ซับซ้อนออกจาก controllers เพื่อให้ controllers ทำหน้าที่เฉพาะการจัดการด้าน Request หรือส่วนที่มีการรับเข้ามาเท่านั้น และในที่สุด service จะเป็นตัวส่งข้อมูล response ที่เหมาะสมกลับไปแทน
- Modules ถือเป็นหน่วยพื้นฐานในการสร้าง web application ด้วย NestJS โดยทำหน้าที่ช่วยแบ่งแยก code ออกเป็นส่วนตามการทำงานหรือ logic ที่เกี่ยวข้องกัน แต่ละ module จะห่อหุ้มไว้ด้วย providers, controllers และส่วนประกอบอื่นๆ ทำให้ application มีลักษณะแยกส่วนออกจากกันได้
1. Controller
Controller ใน NestJS คือ class ที่ถูกเพิ่ม decorator @Controller()
ซึ่งมีหน้าที่รับ HTTP requests ที่ส่งเข้ามา และส่งข้อมูลตอบกลับ (responses) ไปยังฝั่ง Client โดย Controllers จะกำหนด route เพื่อเชื่อม request ต่างๆ เข้ากับ function ที่เรียกว่า handler
โดยที่ handler จะเป็นตัวประมวลผล request และส่งข้อมูล response ในรูปแบบที่เหมาะสมกลับไป
ตัวอย่างของ ProductsController
ที่มีข้อมูล JSON จำลองสำหรับ request แบบ GET
และ POST
โดย ถ้าทุกคนสังเกตเห็น เราจะเห็นตัวที่ชื่อ 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 ใหม่มีดังนี้
2. Provider
Provider ใน NestJS คือ group ที่ประกอบไปด้วย services, repositories, factories และ helpers โดย Providers จะเป็น class ที่มี decorator @Injectable()
กำกับอยู่ ทำหน้าที่รับผิดชอบการจัดการ business logic, การจัดการข้อมูล (data management) และการทำงานอื่นๆ ที่สามารถถูกนำไป inject ให้กับ controllers เพื่อใช้งานได้
โดย Service ก็เป็น Provider ชนิดหนึ่งโดยทั่วไปจะประกอบด้วย method ต่างๆ ที่เกี่ยวข้องกับการจัดการหรือการทำงานของ feature ต่างๆ เช่นตาม ตัวอย่างของ ProductsService
3. Module
Module ใน NestJS คือ class ที่ถูกเพิ่มด้วย @Module()
โดย Module ทำหน้าที่ห่อหุ้ม providers, controllers และ modules อื่นๆ เพื่อสร้าง group ที่ส่วนประกอบต่างๆ มีความเกี่ยวข้องกันให้สามารถเรียกใช้หากันได้
ตัวอย่างของ ProductsModule ที่รวมส่วนของ controller และ service ไว้ด้วยกันมีดังนี้
สุดท้ายเมื่อสร้างทั้งหมดมา เราจะมารวมกันไว้ที่ 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
4. Schema และ Entity
ใน NestJS คำว่า Schema มักหมายถึง Mongoose schema ซึ่งใช้เมื่อมีการทำงานร่วมกับฐานข้อมูล MongoDB โดย Schema จะเป็นตัวกำหนดโครงสร้างของ documents ภายใน collection ของ MongoDB เช่น ชนิดของ field, ค่าเริ่มต้น, validators เป็นต้น โดย Schema เหล่านี้จะถูกนำไปใช้ในการสร้าง Mongoose Model ซึ่งทำหน้าที่เป็นเหมือนตัวเชื่อม (Interface) ในการสื่อสารกับฐานข้อมูล MongoDB เพื่อสร้าง, ค้นหา, อัปเดต หรือลบข้อมูล ของ database ได้
ในขณะที่คำว่า Entity มักจะถูกใช้เมื่อทำงานกับฐานข้อมูล SQL ร่วมกับ TypeORM หรือ Sequelize โดย Entity จะเป็นตัวแทนของ table ในฐานข้อมูล ซึ่งถูกนิยามโดยใช้ class โดยในภาษา TypeScript แต่ละ instance ของ Entity ก็จะเปรียบได้กับหนึ่ง row ในตารางของ database นั่นเอง
ทั้ง Schema และ Entity ทำหน้าที่เป็น data models ที่บอกถึงโครงสร้างของข้อมูล และวิธีการที่จะจัดเก็บและดึงข้อมูลออกจากฐานข้อมูล นับว่ามีความสำคัญมากต่อการรักษาความสมบูรณ์ของข้อมูล (data integrity) และช่วยให้การทำงานต่างๆ กับข้อมูลภายใน web application กับ database สามารถทำงานร่วมกันได้ง่ายขึ้นด้วย
และนี่ก็คือพื้นฐานขององค์ประกอบของ NestJS เพื่อให้เกิดความเข้าใจมากขึ้น เราจะมาลองเล่นผ่านตัวอย่างกัน
มาทำพื้นฐาน RestAPI เบื้องต้นกัน
Start project
เริ่มต้นทำการ start project ผ่าน command ของ NestJS ที่แนะนำใน https://docs.nestjs.com/first-steps
เมื่อทำการ ลง project เรียบร้อย ให้เราทำการเข้า folder project มาแล้ว run ด้วย
และลองเปิดผ่าน localhost:3000 ดูหากเปิดมาได้เรียบร้อยแปลว่า เราลง project อย่างถูกต้องแล้ว
ซึ่ง project เริ่มต้นของ NestJS นั้นจะมีโครงสร้างตามนี้
โดย project นั้นจะเริ่มต้นจาก folder src และเมื่อเปิดมาเราก็จะเจอ 3 อย่าง ตามที่เราอธิบายไปตอนแรกเลยคือ Module, Controller, Service และเมื่อลองเปิด Controller (app.controller.ts
) มาก็จะเจอว่ามีการเรียกใช้ getHello()
อยู่ใน Controller เป็น path เริ่มต้นมา
เมื่อเราลองเปิด Service (app.service.ts
) เราก็จะเจอ AppService ที่เป็นต้นทางของการเรียกใช้งานจาก Controller (ที่ทำการ init ให้ผ่าน Dependency Injection แล้วเรียบร้อย) โดยเป็นการ return text คำว่า Hello World!
ออกมา
และท้ายที่สุดที่ Module (app.module.ts
) ทำการรวมของทั้งหมดเข้าด้วยกันทั้งจาก Controller และ Service
และท้ายที่สุด Module ที่ Export (AppModule
) ออกไปก็ถูกเรียกใช้จากไฟล์หลักเพื่อ start server ที่ main.ts
และนี่ก็คือการทำงานของ 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 โดยสามารถใช้ผ่านคำสั่งเหล่านี้ได้
เราจะลองยกตัวอย่างจากการสร้าง products ด้วยการทำ Mock API กัน โดยการลองใช้คำสั่งสร้าง products Module ขึ้นมาตามนี้
และเมื่อเราลอง run ขึ้นมาก็จะเจอว่าคำสั่งเหล่านี้พยายามไปสร้าง Controller, Service และ Module ไว้ใน folder products
ขึ้นมา
ดังนั้นเมื่อเราลองสำรวจ project structure อีกที ก็จะเจอว่ามี folder ของ products
ขึ้นมาพร้อมกับ pattern มาตรฐาน Controller, Module, Service เป็นที่เรียบร้อย
เมื่อเราลองดูที่ app.module.ts
อีกที ก็จะเจอว่ามีการเรียกใช้งานทั้ง Controller, Service และ Module เข้ามาใน AppModule
หลักเป็นที่เรียบร้อย (จริงๆมันไม่จำเป็นต้องเอาเข้ามาขนาดนี้ก็ได้ หากเราไม่ได้มีความจำเป็นเรียกใช้ Service อะไร ซึ่งเดี๋ยวเราจะมีท่าที่ดีกว่าในการสร้างและจะทำการ import เฉพาะ module เข้ามาได้)
และเมื่อเราไปดูใน 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 หน้าตาประมาณนี้ออกมา
มาที่ Controller products.controller.ts
ทำการสร้าง HTTP Request 2 path คือ
- GET
/products
= สำหรับดึง products ทั้งหมดออกมา - POST
/products
= สำหรับสร้าง products ใหม่และเพิ่มเข้าไปเพื่อเรียกดูจากฝั่ง GET ได้
หลังจากนั้น Module ทำการผูกทุกอย่างรวมกัน โดยทำการเรียกใช้ Service (ที่มีการใช้ Injection เพื่อทำการ auto instance เข้าไปยัง productsService
ที่อยู่ใน Controller) และ Controller มารวมไว้เป็น Module เดียวกัน
หลังจากนั้นเมื่อลองยิง request ก็จะสามารถได้ผลลัพธ์ออกมาตามนี้
และนี่ก็คือตัวอย่างของการทำ HTTP Request จะเห็นว่า ไอเดียคือ
- สร้าง Service สำหรับใช้งานใน API
- สร้าง Controller เป็นตัวแทนของฝั่ง HTTP Request ที่รับมาจาก user (GET / POST)
- สร้าง 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 ไว้เรียบร้อย สามารถลงได้จากคำสั่งนี้เลย
และทำการสร้าง mongodb โดยใช้ docker-compose.yml
ในการสร้างขึ้นมาตามไฟล์นี้ (เป็น mongodb container เพียงตัวเดียว)
หลังจากนั้นทำการ run docker ขึ้นมาโดยใช้คำสั่ง
ก็จะสามารถ run mongo service จาก docker ขึ้นมาได้ เมื่อลอง docker ps
ขึ้นมาและเจอว่า mongo service ขึ้นมาใน docker ได้ = run ถูกต้องแล้วเรียบร้อย
ทีนี้ step ต่อมาเราจะเริ่มสร้างไฟล์สำหรับ service API กัน โดย NestJS เองได้คิดไว้อยู่แล้วว่า Pattern โดยทั่วไปเวลาที่เราทำ API นั้นจะเป็นการทำตาม Pattern ของ CRUD (Resource pattern) ดังนั้น NestJS จึงได้เตรียม CLI อีกตัวหนึ่งซึ่งจะทำการสร้าง CRUD ตัวอย่างขึ้นมาให้ทั้ง set ออกมาได้ โดยการใช้คำสั่งนี้ได้ (หากใครใช้ project เดิมลอง ให้ทำการลบตัวที่ import เพิ่มมาใน app.module.ts
ก่อนใช้คำสั่ง และลบของที่อยู่ใน folder 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 จะเหลือออกมาหน้าตาประมาณนี้ออกมา
เมื่อลองดูในแต่ละไฟล์สังเกตว่าจะยังเป็นเพียง mock ของแต่ละตัวออกมา หากเราลอง run ใหม่และลองยิงออกมาก็จะเจอ response ตัวอย่างหน้าตาประมาณนี้ออกมาได้
สุดท้ายหลังจากสร้าง resource มาได้เรียบร้อย ก่อนที่เราจะเริ่มทำ CRUD API จริงๆ เราจำเป็นต้อง config mongodb เข้า NestJS โดยเราจะทำการต่อ mongodb ผ่าน Mongoose เข้ามาใน config ของ NestJS ที่ app.module.ts โดยให้ AppModule ทำการ import module ของ Mongoose เข้ามา เพื่อให้ทุกที่ภายใน Application สามารถใช้งาน Mongoose module ให้เป็นตัวแทนของ Mongodb ได้
ที่ app.module.ts
ก็จะมีหน้าตาประมาณนี้
เมื่อทำการ 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
ก็จะมีหน้าตาเป็นแบบนี้
ที่ dto เราจะทำการปรับเพื่อให้สามารถรับ request json 3 fields ที่หน้าตาประมาณนี้ออกมาได้
ดังนั้น ส่วนของ create-product.dto.ts
และ update-product.dto.ts
ก็จะมีหน้าตาเป็นแบบนี้ออกมาได้ (ทั้ง Create และ Update request เหมือนกัน เราจะใช้เพียง id ตรง url ในการแยก case เท่านั้น)
- โดยสัญลักษณ์
?
เป็นการบอกว่าเป็น optional โดยจะสามารถส่งหรือไม่ส่งมาก็ได้
ทีนี้ส่วนสำหรับ Controller และ DTO ก็เรียบร้อย ต่อมากำหนด schema ของ Product ต่อ เพื่อเป็นต้นแบบของโครงสร้างข้อมูลใน collection ของ Product เพื่อใช้งานใน Service ที่ schemas/product.schema.ts
โดยจาก dto ด้านบน เราจะทำการเก็บ field เหมือนกันลงใน database คือ
name
เก็บชื่อ product เป็นประเภท stringdescription
เก็บคำอธิบาย product เป็นประเภท stringprice
เก็บราคา product เป็นประเภท number
หน้าตาของ schema ก็จะออกมาเป็นประมาณนี้
กลับมาที่ products.service.ts
เราจะทำการสร้าง function สำหรับ code CRUD ขึ้นมา โดย funtion ในนี้จะเป็น function ที่ถูกเรียกใช้จาก Controller และใช้ Schema เป็นตัวแทนสำหรับการ สร้าง / แก้ไข / อ่าน / ลบ ข้อมูล Product ออกมาได้ Service ที่รวม CRUD ไว้ก็จะมีคำสั่งประมาณนี้ (คำสั่งนี้ดูเพิ่มเติมใน Document ของ mongoose ได้ รวมถึงสามารถกลับไปดูในหัวข้อของ Mongoose ของช่อง Mikelopster ได้เช่นกัน)
สุดท้าย กลับมาที่จุดรวมพล products.module.ts
ทำการเรียกใช้ Service, Controller ทั้งหมด รวมถึงเรียกใช้ MongooseModule.forFeature
เพื่อทำการเชื่อมต่อ Mongoose กลับไปยัง MongoDB เพื่อบอกว่ามีการใช้ Schema Product เป็นตัวแทนในการสื่อสารกลับไปยัง product collection ใน mongo
MongooseModule.forFeature
เป็น method ที่อยู่ภายใน package@nestjs/mongoose
โดยจะใช้ในการ configMongooseModule
และระบุว่า 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 เข้าไป
และนี่คือผลลัพธ์ที่เราทำมา เมื่อ run ด้วย npm run start:dev
อีกรอบ (หรือจริงๆไม่ต้อง run ใหม่ก็ได้เนื่องจากคำสั่ง npm run start:dev
ได้ทำการ watch file ไว้อยู่แล้ว) แล้วเราลองยิงตาม Path ของ Controller ดูก็จะได้ผลลัพธ์ API แบบฉบับของ CRUD ต่อเข้ากับ Mongo ออกมาได้
เพิ่ม 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 เข้าไป
ที่ dto/create-product.dto.ts
และ dto/update-product.dto.ts
ทำการปรับการใช้ schema จากแต่เดิม ที่มีการประกาศประเภทไว้เฉยๆ ทำการเรียกใช้การ validate ผ่าน pacakge class-validator
เข้ามา
และ เพื่อให้การ 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 มีความปลอดภัยได้ดีขึ้นด้วยเช่นกัน
และนี่คือผลลัพธ์ หลังจากที่เราเพิ่ม validation เข้าไปให้ลองทดสอบส่งข้อมูลผิดประเภทเข้าไปดูเช่น ส่ง price เป็น string เข้าไปแทน ก็จะเจอ error ออกมาได้
เพิ่มใช้งานร่วมกับ 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
แทน
หลังจากนั้น เราก็จะเจอ folder ของ orders
ที่มี file structure เหมือนๆเดิมออกมา แต่เนื่องจากเคสนี้เราไม่ได้มีการทำเคสแก้ไขของ Order ดังนั้นทำการลบ update-order.dto.ts
ไปเนื่องจากไม่ได้ใช้งานในเคสนี้ (รวมถึงจะขอลบไฟล์ที่เกี่ยวกับการ Test ทิ้งก่อน เพื่อให้ตัวอย่างเห็นภาพง่ายขึ้น) ดังนั้น file structure ก็จะมีหน้าตาแบบนี้ออกมา
หลังจากนั้นเริ่มเหมือนเดิม จาก Controller และ DTO ปรับให้ตรงตามโจทย์ของเราโดยจะเหลือเพียงแค่ @Post()
และ @Get(':id')
แค่ 2 API นี้ และจะทำการเรียกใช้ service ตามชื่อเดิมที่ Resource ได้ทำการเตรียมไว้ให้
ที่ orders.controller.ts
ก็จะมี code หน้าตาแบบนี้
ต่อมาที่ DTO ปรับให้ตรงตามโจทย์และเพิ่มการ validation เข้าไปที่ create-order.dto.ts
โดยจะรับเพียง 2 fields คือ
productId
เป็น string สำหรับการเก็บ Id ของ Product ไว้number
เป็น number สำหรับเก็บจำนวนของ Product ที่สั่งซื้อไว้
code DTO ก็จะหน้าตาประมาณนี้
ฝั่ง Controller + DTO เสร็จเรียบร้อย ต่อมาเราจะทำการสร้าง schema เพื่อใช้สำหรับเป็นต้นแบบของ Order Schema ในการเก็บข้อมูลเข้า MongoDB
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 fieldproductId
กลับไปยัง model Product เพื่อนำข้อมูล product ออกมาและทำการส่งออกผ่าน response กลับไป
สุดท้ายรวมทั้งหมดที่ Module orders.module.ts
โดยในรอบนี้เราจะทำการรับ ProductsModule
เข้ามาด้วย
ProductsModule
รับเข้ามาเพื่อทำการใช้คำสั่งProductsService
สำหรับคำสั่งจัดการข้อมูลใน modelProduct
ต่างๆเข้ามา (เช่น findOne ที่เรามีการใช้ในกรณีด้านบนใน service ของ Order)
ดังนั้นที่ code orders.modules.ts
ก็จะมีหน้าตาประมาณนี้
และที่ฝั่งของ products.module.ts
เองก็จะต้อง export ทั้ง ProductsService
ออกมาเช่นกัน
ก็จะได้ผลลัพธ์ตามโจทย์ที่เราต้องการออกมาได้
เรียกใช้ config ผ่าน .env
Ref: https://docs.nestjs.com/techniques/configuration
สุดท้ายเพื่อให้เกิดความเป็นระเบียบเรียบร้อยของ config เราจะทำการย้าย mongo config มาไว้ที่ Environment Variable กัน โดยใน NestJS เองได้เตรียม package ไว้ให้แล้วนั่นก็คือ @nestjs/config
ที่ .env
ให้ทำการเพิ่ม config จากแต่เดิมที่เราเคยมีการ Fix ไว้ตอนแรกเข้ามา
สุดท้ายที่ app.module.ts
ให้ทำการเรียกใช้ env ผ่าน ConfigModule, ConfigService ใน @nestjs/config
ออกมา
ConfigModule
import จาก@nestjs/config
ใช้สำหรับการ handle environment variables จาก.env
ConfigService
ทำหน้าที่ในการดึงค่า environment variable โดยใช้ methodget
ตัวอย่างเช่นconfigService.get<string>('MONGODB_URI')
สำหรับดึงค่าจากMONGODB_URI
ใน.env
- ใน
MongooseModule.forRootAsync
ตัวเลือก inject ระบุว่าเราต้องการให้ConfigService
ถูก inject ให้กับ functionuseFactory
เพื่อให้สามารถใช้งานconfigService
ในuseFactory
ได้
code ของ app.module.ts
ก็จะมีหน้าตาประมาณนี้ และหากผลลัพธ์ออกมาเหมือนเดิมได้ เท่ากับว่าเราได้ config ทุกอย่างไว้อย่างถูกต้องแล้วเรียบร้อย ก็จะสามารถเรียกใช้ผ่าน .env
แทนการ hard code ออกมาได้
สรุป
และนี่ก็คือ 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 กันนะครับ
- มาแก้ปัญหา Firestore กับปัญหาราคา Read pricing สุดจี๊ดมี Video มี Github
ในฐานะที่เป็นผู้ใช้ Firebase เหมือนกัน เรามาลองชวนคุยกันดีกว่า ว่าเราจะสามารถหาวิธีลด Pricing หรือจำนวนการ read ของ Firestore ได้ยังไงกันบ้าง
- มาลองเล่น LIFF และ Messaging API กันมี Video มี Github
พามาทำความรู้จักกับ LIFF (LINE Frontend Framework) กันว่ามันคืออะไร เราสามารถพัฒนา Web app ลงบน LINE ได้อย่างไร
-
- ลองเล่น Supabase กับ Next.js กันมี Video มี Github
รู้จักกับ Supabase เทคโนโลยีฐานข้อมูลที่เรียกตัวเองว่าเป็น Firebase alternative กันว่าใช้ทำอะไรได้บ้าง