The app module
สิ่งที่จะสร้าง
หัวข้อที่มีชื่อว่า “สิ่งที่จะสร้าง”สองไฟล์ ที่ Backend init scaffold ไว้แล้ว: apps/api/src/main.ts และ apps/api/src/app.module.ts บทเรียนนี้แทนที่ controller Hello World! เดิมด้วย endpoint GET /health จริง และเพิ่ม call สองตัวที่สำคัญกับ production เข้าไปใน bootstrap — enableCors กับ enableShutdownHooks ทุกโมดูลถัดไปในคอร์สนี้จะเพิ่มของลงในสองไฟล์เดิมนี้ ไม่ใช่เขียนทับใหม่
NestJS module คือคลาสที่ใช้ @Module() ประกาศสามอย่าง: เป็นเจ้าของ provider ตัวไหน (providers), expose controller ตัวไหน (controllers) และต้องพึ่ง module อื่นตัวไหน (imports) AppModule คือรากของกราฟนั้น ทุก feature module ที่คอร์สนี้จะสร้างต่อไป (PostsModule, UsersModule และภายหลัง TagsModule, CommentsModule) จะเข้าไปอยู่ใน imports array ของ AppModule เหมือนที่ ConfigModule เข้าไปตั้งแต่ Backend init
นี่คือครึ่งหนึ่งของระบบ module ที่เรียกว่า composition: แทนที่จะมีไฟล์เดียวเชื่อม controller และ service ทุกตัวด้วยมือ แต่ละ module compose ส่วนของแอปที่ครบในตัวเอง แล้ว AppModule compose module เหล่านั้นเข้าด้วยกัน อีกครึ่งคือ dependency injection AppController ไม่ได้สร้าง AppService เองด้วย new AppService() แต่ประกาศ constructor parameter ที่ type เป็น AppService แล้ว Nest IoC container จะหา (หรือสร้าง) instance เดียวที่แชร์กันส่งเข้ามาให้:
constructor(private readonly appService: AppService) {}นี่คือ pattern Dependency Injection / Inversion of Control แบบคลาสสิก: controller พึ่งพา abstraction (ในที่นี้คือแค่คลาส AppService เพราะ Nest ไม่บังคับให้ใช้ interface สำหรับ case ง่าย ๆ) แทนที่จะพึ่งขั้นตอนสร้าง instance ที่เป็นรูปธรรม และมีบางอย่างจากภายนอก — container — เป็นคนส่ง instance ให้ ผลลัพธ์เห็นได้ทันทีตอน testing (test module สามารถสลับ AppService ปลอมเข้ามาแทนได้โดยไม่ต้องแตะโค้ดของ AppController เลย) และเห็นอีกครั้งภายหลังในคอร์สนี้ ตอนที่ Authentication สลับ UsersService จริงเข้าไปใน JwtStrategy ด้วยวิธีเดียวกันเป๊ะ ๆ
ConfigModule.forRoot({ isGlobal: true, ... }) จาก Backend init คือเหตุผลที่ main.ts ด้านล่างเรียก app.get(ConfigService) ได้โดยที่ AppModule ไม่เคย import ConfigModule เข้า imports array ของ feature module ไหนเลย เพราะ global module ลงทะเบียนสิ่งที่ export ไว้บน root container ซึ่งเข้าถึงได้จากทุกที่
ข้อดีข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีข้อเสีย”DI container เทียบกับการเชื่อมด้วยมือ การ new AppService() เองใน constructor ของ AppController ก็ใช้ได้กับคลาสเล็ก ๆ แบบนี้ แถมเลี่ยงความงงแบบ “ตกลงตัวนี้ถูกสร้างขึ้นมายังไง” ที่คนเพิ่งเริ่มใช้ Nest มักเจอ แต่วิธีนี้พังทันทีที่ AppService ต้องการ dependency ของตัวเอง (ConfigService, Mongoose model) เพราะทุกจุดที่เรียก new AppService() ต้องรู้วิธีสร้าง dependency นั้นด้วย แล้วกราฟของ object ก็บานเร็วมาก ส่วน container สร้างกราฟทั้งหมดครั้งเดียว เป็น singleton โดย default และแต่ละคลาสแค่ประกาศว่าต้องการอะไร ไม่ต้องรู้วิธีสร้าง
enableCors({ origin: WEB_ORIGIN, credentials: true }) เทียบกับ enableCors() enableCors() เปล่า ๆ จะ reflect origin อะไรก็ได้ ซึ่งใช้ได้กับ demo local แต่ไม่ใช่สิ่งที่ browser จะยอมด้วยซ้ำเมื่อมี cookie หรือ header Authorization เข้ามาเกี่ยวข้อง: CORS spec ห้าม credentials: true จับคู่กับ origin แบบ wildcard การระบุ origin เดียวชัด ๆ จึงไม่ใช่แค่ปลอดภัยกว่า แต่จำเป็นทันทีที่ Authentication เริ่มส่ง request แบบมี credential จากแอป Next.js ต้นทุนคือ environment variable อีกตัว (WEB_ORIGIN มีอยู่แล้วใน .env.example จาก Repo layout) ที่ต้องคอยให้ตรงกันระหว่างสองแอป
ติดตั้ง
หัวข้อที่มีชื่อว่า “ติดตั้ง”อัปเดต apps/api/src/main.ts ให้เปิด CORS และ shutdown hooks ควบคู่ไปกับ port ที่มาจาก ConfigService เดิม:
import { NestFactory } from '@nestjs/core';import { ConfigService } from '@nestjs/config';import { AppModule } from './app.module';
async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService);
app.enableCors({ origin: configService.getOrThrow<string>('WEB_ORIGIN'), credentials: true, });
app.enableShutdownHooks();
const port = configService.get<number>('API_PORT') ?? 4000; await app.listen(port); console.log(`API listening on http://localhost:${port}`);}bootstrap();enableCors— จำกัด request จาก browser ให้เหลือแค่ origin เดียวที่แอป Next.js รันอยู่ (WEB_ORIGIN) พร้อมcredentials: trueเพื่อให้ cookie/headerAuthorizationข้าม origin ได้เมื่อมี authenableShutdownHooks— ถ้าไม่เรียก call นี้ lifecycle hookonModuleDestroy/onApplicationShutdownจะไม่ทำงานเมื่อได้รับSIGTERMเลย hook ชุดนี้มี overhead ของ listener เล็กน้อยจึงปิดไว้เป็น default แต่นี่แหละคือสิ่งที่ทำให้โมดูลถัดไปปิด Mongoose connection หรือ flush queue ได้อย่างสะอาด แทนที่จะถูก kill กลางคันตอนกำลังเขียนข้อมูล เมื่อ container orchestrator ส่งSIGTERMมา
แทนที่คู่ AppController/AppService ที่ scaffold ไว้ อัปเดต apps/api/src/app.controller.ts:
import { Controller, Get } from '@nestjs/common';import { AppService } from './app.service';
@Controller()export class AppController { constructor(private readonly appService: AppService) {}
@Get('health') getHealth(): { status: string } { return this.appService.getHealth(); }}อัปเดต apps/api/src/app.service.ts:
import { Injectable } from '@nestjs/common';
@Injectable()export class AppService { getHealth(): { status: string } { return { status: 'ok' }; }}app.module.ts ยังไม่ต้องแก้อะไร — AppController และ AppService ถูกเชื่อมไว้แล้วจาก Backend init:
import { Module } from '@nestjs/common';import { ConfigModule } from '@nestjs/config';import { AppController } from './app.controller';import { AppService } from './app.service';
@Module({ imports: [ ConfigModule.forRoot({ isGlobal: true, envFilePath: '../../.env', }), ], controllers: [AppController], providers: [AppService],})export class AppModule {}ตรวจสอบผล
หัวข้อที่มีชื่อว่า “ตรวจสอบผล”npm run start:dev[Nest] ... LOG [NestApplication] Nest application successfully startedAPI listening on http://localhost:4000ในเทอร์มินัลอีกอัน:
curl localhost:4000/health# {"status":"ok"}AppController ที่คืนค่า {"status":"ok"} ยืนยันว่ากราฟ DI resolve สำเร็จ — AppController ได้ instance ของ AppService จริง ๆ โดยไม่มีฝั่งไหนเชื่อมด้วยมือเลย — และ enableCors/enableShutdownHooks ไม่ได้ทำให้ startup พัง ตอนนี้ยังไม่มี browser request ให้เห็น CORS ปฏิเสธ origin ที่ผิดจริง ๆ นั่นจะเห็นได้ชัดตอนที่ Frontend Foundations ยิง request จริงจากแอป Next.js
AppModule compose provider, controller และ (ในที่สุด) feature module เข้าเป็นกราฟเดียว; Nest IoC container resolve กราฟนั้นแล้ว inject dependency ที่แต่ละคลาสประกาศไว้แทนที่จะให้คลาสสร้างกันเองด้วยมือ main.ts ตอนนี้เปิด CORS ที่จำกัดแค่ WEB_ORIGIN พร้อม credentials และ shutdown hooks เพื่อให้ lifecycle cleanup ทำงานตอน SIGTERM ส่วน GET /health เข้ามาแทน Hello World! ที่ scaffold ไว้ กลายเป็น endpoint จริงตัวแรกของ API ที่ทุกโมดูลถัดไปจะต่อยอดหรือเพิ่มของเข้าไปข้าง ๆ
ถัดไป: Mongoose connection →