ต้องบอกว่าผมเองก็อยู่กับเฟรมเวิร์ก Laravel ที่ใช้ภาษา PHP มานาน การที่ตัวเองมาต้องออกจาก Comfort Zone มาเขียน API ด้วยเฟรมเวิร์กภาษา TypeScript อย่าง NestJS เป็นเรื่องที่ท้าทายเป็นอย่างมาก ทั้งเรื่องโครงสร้างไฟล์ที่แตกต่าง รวมถึง Syntax ของ TypeScript ที่ต้องบอกเลยว่า อาจจะต้องปรับตัวกันเยอะหน่อยนะ

ผมโชคดีจังเลยที่ตอนเรียนในมหาวิทยาลัยได้เรียนภาษา Java ซึ่งเป็นภาษาที่เน้นเรื่องการใช้ Static Type และ Object-orented Programming และการเขียนเว็บ Frontend ด้วย JavaScript ทำให้ผมนำความรู้ที่มีมาใช้ในการเขียน RESTFul API เพื่อเชื่อมต่อฐานข้อมูลครับ

Technology Stack ที่ใช้

Stack ที่ผมใช้มาทำเป็น Example ก็จะมี

  • NestJS เป็น Framework ภาษา TypeScript ที่มีฟังก์ชันให้เลือกใช้อย่างครบครันพอ ๆ กับ Laravel ที่เขียนอยู่เลยครับ
  • MongoDB เป็นฐานข้อมูลแบบ NoSQL ที่ให้อิสระในการออกแบบข้อมูล ซึ่งทำให้ผมแทบจะเลิกสนใจการออกแบบฐานข้อมูลใน MySQL/MariaDB ไปได้
  • Docker เป็น Container Engine ที่ทำให้ผมเลิกใช้ MAMP XAMPP ในการเขียนโค้ดภาษา PHP ตลอดกาล

ความคิดเห็นส่วนตัว

ในทีนี้ผมจะใช้ Docker เพื่อเรียกใช้ MongoDB อย่างเดียว ส่วน Backend ผมเขียนบน Local ผมไม่อยากลงแรงแบบบทความที่ผมเคยเขียนไว้มาก ผมเลยให้มันรันผ่าน Node ที่ติดตั้งไว้ในเครื่องดีกว่าครับ

วิธีสร้างโปรเจกต์ NestJS

เริ่มต้นจากลง Node.js และ Yarn ในเครื่องให้ได้ก่อน ละก็ ติดตั้ง NestJS CLI ด้วยคำสั่งนี้ครับ

yarn global add @nestjs/cli

ติดตั้งสิ่งที่จำเป็นในการทำ API กันแล้วมาสร้างโปรเจกต์ ด้วยคำสั่งนี้ครับ (แทนที่ข้อความ nestjs-mongodb ด้วยชื่อโปรเจกต์คุณเลยครับ)

nest new nestjs-mongodb

ทีนี้มันจะถามครับว่าใช้ตัวจัดการแพคเกจอะไรกับโปรเจกต์นี้ เลือก yarn ได้เลยครับ

สร้างโปรเจกต์กันเสร็จแล้ว ทีนี้เราจะลงแพคเกจที่จำเป็นเพิ่ม ก็จะมี

  • typeorm เป็นแพคเกจที่ช่วยในการเขียน Query เพื่อเรียกข้อมูลในฐานข้อมูลด้วย Syntax ภาษา TypeScript ซึ่งใช้ได้ทั้งฐานข้อมูล MySQL MariaDB และ MongoDB อีกด้วยครับ
  • @nestjs/typeorm เป็นแพคเกจที่จะทำให้ TypeORM สามารถใช้ร่วมกับ NestJS ได้ครับ
  • mongodb อันนี้เป็น Engine ที่ทำให้ TypeORM สามารถต่อฐานข้อมูล TypeORM ได้ครับ
  • @nestjs/config เป็นแพคเกจที่ทำให้สามารถอ่านค่าจากไฟล์ .env มาใช้ในโค้ดได้ครับ

ทีนี้มาลง Package เสริมในโปรเจกต์เรากันครับ

yarn add @nestjs/config @nestjs/typeorm typeorm mongodb

วิธีเรียกใช้ค่า .env ใน main.ts ของ NestJS

วิธีที่ทำให้สามารถใช้ .env ใน NestJS ได้ เรามาเริ่มจากการสร้างไฟล์ .env ที่ Root ของ Project นี้เลยครับ จากนั้นก็เขียนตาม Syntax ได้ประมาณนี้ครับ ส่วนค่าของฐานข้อมูลที่จะใส่ก็ตามที่ตั้งค่าไว้เลยครับ

APP_PORT=3000

DATABASE_HOST=localhost
DATABASE_PORT=27017
DATABASE_USERNAME=root
DATABASE_PASSWORD=phongphan.me
DATABASE_NAME=nestjs-mongodb

ได้เวลามาเขียน TypeScript แล้วครับ มาเริ่มต้นจากการให้สามารถเรียก API จาก NestJS ด้วย Port ที่กำหนดใน .env ให้ไปยัง 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();

วิธีการเรียกค่าจาก .env ใน src/main.ts ไม่ยากเลยครับ เริ่มต้นจากการเพิ่ม Statement เพื่อ Import คลาส ConfigService ของแพคเกจ @nestjs/config

import { ConfigService } from "@nestjs/config"; //เพิ่มบรรทัดนี้
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";

จากนั้น เพิ่ม Statement นี้เพื่อเรียกค่า Config จาก ConfigService ครับ

async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    const configService = app.get(ConfigService); //เพิ่มบรรทัดนี้
    await app.listen(3000);
}

เปลี่ยนเลข Port ที่เขียนฝังไว้ในโค้ดด้วย Statement configService.get<number>("APP_PORT", 3000) ครับ

async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    const configService = app.get(ConfigService);

    await app.listen(configService.get<number>("APP_PORT", 3000)); //แก้บรรทัดนี้
}

จาก Statement ด้านบน

  • number ก็คือชื่อ Data Type ตัวเลขเพื่อบอกว่า ค่าที่เรียกมาจาก Config นั้นเป็นตัวเลขนะ เราก็จะเปลี่ยนไปตามค่า Config ที่เราใส่อาจจะเป็นอย่างอื่น เช่น string หรือ boolean ขึ้นอยู่กับว่าฟังก์ชันนั้นต้องการรับ Parameter หรือตัวแปรที่ตั้งไว้ เป็น Data Type ชนิดอะไร
  • APP_PORT ก็คือชื่อ Config ใน .env ที่เราต้องการนำค่ามาใส่
  • 3000 จะใส่หรือไม่ใส่ก็ได้นะ มันคือค่าเริ่มต้น (Default) เมื่อไม่เจอชื่อ Config ที่ระบุใน .env ครับ มันก็จะหยิบค่า Default มาใช้แทน อันนี้มีประโยชน์ตอนที่มัน Error ได้ตอนที่หาค่า Config ไม่เจอครับ โดยที่ Data Type ของ Parameter นี้ต้องตรงกับที่ระบุไว้ใน Generic Type ของฟังก์ชันด้วยนะครับ ก็คือค่าในวงเล็บ <…> นี่ละครับ

สุดท้าย ไปยัง src/app.module.ts เพื่อให้ทั้งโปรเจกต์สามารถเรียกใช้ Config จาก .env ได้ เราต้องเรียกใช้ ConfigModule.forRoot() เข้าไปยัง Array ของ Property imports ใน Object ของ Decorator @Module ด้วยนะครับ

import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";

@Module({
    imports: [
        ConfigModule.forRoot() //เพิ่มบรรทัดนี้
    ],
    controllers: [AppController],
    providers: [AppService]
})
export class AppModule {}

การเรียกใช้ค่า .env คู่กับการเชื่อมต่อฐานข้อมูลด้วย TypeORM

เพื่อให้เห็นภาพมากขึ้นในการเรียกใช้ Config ใน ไฟล์ .env ผมจะขอยกตัวอย่างในการเชื่อมต่อฐานข้อมูลด้วย TypeORM ใน AppModule ประมาณนี้ครับ

import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { TypeOrmModule } from "@nestjs/typeorm";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
@Module({
    imports: [
        TypeOrmModule.forRootAsync({
            imports: [ConfigModule],
            useFactory: (configService: ConfigService) => ({
                type: "mongodb",
                host: configService.get<string>("DATABASE_HOST", "localhost"),
                port: configService.get<number>("DATABASE_PORT", 27017),
                authSource: "admin",
                username: configService.get<string>(
                    "DATABASE_USERNAME",
                    "phongphan.me"
                ),
                password: configService.get<string>("DATABASE_PASSWORD", ""),
                database: configService.get<string>(
                    "DATABASE_NAME",
                    "nestjs-mongodb"
                ),
                autoLoadEntities: true
            }),
            inject: [ConfigService]
        }),
        ConfigModule.forRoot()
    ],
    controllers: [AppController],
    providers: [AppService]
})
export class AppModule {}

จะเห็นว่า ตอนที่เราเรียก Module ของ TypeORM ด้วย TypeOrmModule.forRootAsync จะประกอบด้วย 3 Property เลยคือ

  1. imports เราต้อง Import คลาส ConfigModule เพื่อให้ AppModule สามารถเอาค่า Option ของ TypeORM ที่ตั้งใน Property useFactory มาใช้กับ NestJS ได้
  2. useFactory เหมือนเป็นการสร้าง Factory Function เพื่อให้สามารถนำค่าที่ Config ภายในฟังก์ชันไปเป็นส่วนหนึ่งของ TypeORM ซึ่งในทีนี้ เราสามารถที่จะเขียน Logic บางอย่างเข้าไปได้ หรือถ้าทำให้โค้ดดูรกเกินไป สามารถเขียนคลาส Service เพิ่มเพื่อให้โค้ดดู Clean ได้ และเรียกใช้คลาสที่เขียนด้วย Property useClass แทนได้
  3. inject จะเป็นการเอาคลาส Service ก็คือ ConfigService มาใช้ใน Factory Function เพื่อให้สามารถเรียกใช้ค่า Config ในฟังก์ชันได้ หรือถ้าใช้ Property useClass Property inject อาจไม่ต้องใช้ก็ได้ครับ

ทีเหลือก็ต้องเขียน Repository Service รวมถึงเขียน Controller เพื่อให้สามารถเรียกใช้ API ได้ ซึ่งการใช้ TypeORM คู่กับ MongoDB ใช้ไม่ยากเลยครับ ขอจบบทความนี้ด้วยรูปการยิง API ซึ่งเป็นผลที่ได้จากการเรียนรู้ด้วยตนเองของผม หวังว่าจะเป็นประโยชน์ให้คนที่มือใหม่เหมือนกันนะครับ

การยิง API ที่สร้างด้วย NestJS ผ่านโปรแกรม Postman โดยเชื่อมต่อฐานข้อมูล MongoDB ผ่าน TypeORM
การยิง API ที่สร้างด้วย NestJS ผ่านโปรแกรม Postman โดยเชื่อมต่อฐานข้อมูล MongoDB ผ่าน TypeORM

Author

พงษ์พันธุ์เป็นคนธรรมดาที่รักการพัฒนาเว็บ ตลอดเวลาที่ว่างเขาฝึกฝนตนเองในการพัฒนาเว็บเป็นประจำ เช่น เรียนรู้สิ่งใหม่ ๆ เกี่ยวกับเทคโนโลยีเว็บ และค้นหาวิธีที่ดีที่สุดในการทำงานร่วมกันเพื่อให้ผู้อื่นเข้าใจตรงกัน และทำงานร่วมกันให้สำเร็จ