สวัสดีครับ สำหรับวันนี้เอาใจสาย Backend ที่ทำโปรเจกต์โดยใช้ NestJS แล้วอยากรู้ว่าหากเรามีโปรเจกต์ NestJS ที่ต่อกับฐานข้อมูลแล้วอยากแพ็คใส่ Docker ด้วยมันจะมีขั้นตอนการทำยังไงบ้าง เดี๋ยวเรามาดูไปพร้อมกันในบทความนี้ได้เลยครับ
พื้นฐานที่ต้องมีก่อนจะทำตามบทความนี้
1. การเขียนโปรแกรมด้วยภาษา TypeScript เพราะ NestJS ใช้ TypeScript เป็นหลัก
3. Docker ควรเข้าใจการทำงาน เช่น Dockerfile > Docker Image > Docker Container เบื้องต้นมาก่อน
3. PostgreSQL
เริ่มต้นสร้างฐานข้อมูลจาก Docker
โดยผมจะทำการสร้างฐานข้อมูล PostgreSQL ด้วย Docker Compose ไฟล์ชื่อว่า docker-compose.yaml จากโค้ดด้านล่างนี้
version: '3.8'
services:
db:
image: postgres
restart: always
environment:
POSTGRES_USER: yourusername
POSTGRES_PASSWORD: yourpassword
POSTGRES_DB: yourdatabase
ports:
- "5432:5432"
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:
หลังจากนั้นทำการรันโดยการไปที่ Terminal ของ path ที่มีไฟล์โค้ดด้านบนแล้วใช้คำสั่ง
docker compose up -d
หลังจากนั้นสามารใช้คำสั่ง docker ps –filter “ancestor=postgres” เพื่อหา Container ที่กำลังรันอยู่ด้วย Image ชื่อว่า postgres
ถ้าได้ตามภาพด้านบนแล้ว เราก็ลองเปิดโปรแกรม pgAdmin หรือโปรแกรมอื่นที่ใช้ในการดูฐานข้อมูลได้ แล้วเชื่อมต่อตาม connection ที่เราได้ทำการเขียนไว้ในไฟล์ Docker Compose
Host name : localhost
Port : 5432
Username : yourusername
Password: yourpassword
โอเคครับ ตอนนี้เราก็ได้ทำการเซ็ต Database เตรียมพร้อมสำหรับใช้งานเรียบร้อยแล้ว ต่อไปเราไปสร้างโปรเจกต์ NestJS กันครับ โดยการเริ่มจากติดตั้ง NestJS CLI ด้วยคำสั่ง
npm install -g @nestjs/cli
สร้างโปรเจกต์ของ NestJS ใหม่โดยใช้ชื่อโปรเจกต์ว่า example-crud-product
npx nest new example-crud-product
เลือก package manager ที่ต้องการใช้งานเช่น npm, yarn หรือ pnpm โดยตัวอย่างนี้เราจะใช้ npm
เสร็จแล้วเราก็จะได้โปรเจกต์ Default ของ NestJS มาเป็นที่เรียบร้อย
เสร็จแล้วเราก็ cd เข้าไปที่โปรเจกต์และรันด้วยคำสั่ง npm run start ได้เลย
เชื่อมต่อ NestJS กับ PostgreSQL ด้วย Prisma
ติดตั้ง Prisma ด้วยคำสั่ง
npm install @prisma/client prisma
โดย Prisma เป็นเครื่องมือที่ช่วยให้สามารถทำงานกับฐานข้อมูลได้โดยไม่ต้องเขียน SQL โดยตรง ซึ่งช่วยให้สามารถทำงานกับข้อมูลโดยใช้ภาษาที่เราเขียนได้เลย เช่น TypeScript, JavaScript
โครงสร้างของโปรเจกต์ NestJS
โดยทั่วไปแล้วหากเราสร้างโปรเจกต์ NestJS จะได้โฟลเดอร์และไฟล์ดังนี้มา 👇
📂 โฟลเดอร์:
- node_modules: อันนี้สาย Node.js น่าจะรู้จักดี เอาไว้เก็บ dependacy ต่างๆ
- src: โฟลเดอร์ที่เก็บโค้ด TypeScript ของ NestJS application ของเรา
- tests: โฟลเดอร์สำหรับเก็บโค้ดเทส
- dist: ไฟล์หรือของที่ build แล้ว
🗃️ ไฟล์ใน src (สำหรับตอนนี้ให้โฟกัสที่ src ก่อนนะครับ):
app.controller.spec.ts: ไฟล์เทสสำหรับ AppController
app.controller.ts: ไฟล์ AppController (ตัวควบคุมหลัก) ของแอป
app.module.ts: ไฟล์สำหรับโมดูลของแอป
app.service.ts: ไฟล์สำหรับ AppService (บริการหลัก) ของแอป
main.ts: ไฟล์ที่เป็นจุดเริ่มต้นของแอป
เริ่มแรกให้เราไปที่ไฟล์ app.module.ts
แล้วทำการลบ AppController และ AppService ซะ เพราะเราไม่จำเป็นต้องใช้ร่วมถึงที่ import และไฟล์ app.controller.ts, app.controller.spec.ts และ app.service.ts ด้วย
ต่อมาใช้คำสั่ง npx prisma init เพื่อสร้างไฟล์ schema.prisma ในโฟลเดอร์ prisma สำหรับกำหนด data model ขึ้นมา คล้ายกับกำหนดตารางในฐานข้อมูลและกำหนดค่า Prisma ในการต่อกับฐานข้อมูล
หลังจากจะได้ไฟล์ schema.prisma ในโฟลเดอร์ prisma
ต่อมาก็เอาค่าของฐานข้อมูลเราไปใส่ให้ตรงใน .env
กำหนด data model ในฐานข้อมูลสำหรับตาราง “Product”
ต่อมาเราสามารถสั่งให้สร้างตาราง “Product” ในฐานข้อมูลให้ตรงกับ data model ในไฟล์ schema.prisma โดยการใช้คำสั่ง npx prisma migrate dev –name init
เราจะเห็นได้ว่าในฐานข้อมูลจะมีตารางตามที่เรากำหนดไว้แล้ว
ต่อมาให้เรากลับไปที่โฟลเดอร์ src สร้างไฟล์ชื่อว่า prisma.service.ts ขึ้นมา
PrismaService นี้เป็นสร้างการเชื่อมต่อกับฐานข้อมูล PostgreSQL โดยใช้ Prisma ตอนที่ NestJS สตาร์ทแอปขึ้นมา โดย PrismaService จะเป็นตัวจัดการทุกๆ การสื่อสารกับฐานข้อมูล
ต่อมาเราจะมาสร้าง CRUD ของโปรเจกต์ NestJS กัน
ต่อมาในโฟลเดอร์ src ให้เราสร้างโฟลเดอร์ชื่อว่า product ขึ้นมาแล้วสร้างไฟล์ตามภาพด้านบนเมื่อเราสร้างไฟล์ โดยโค้ดของแอปแต่ละส่วยของ NestJS จะมีดังนี้
product.service.ts
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma.service';
import { Product } from './product.model';
@Injectable()
export class ProductService {
constructor(private prisma: PrismaService) {}
async getAllProducts(): Promise<Product[]> {
return this.prisma.product.findMany();
}
async getProduct(id: number): Promise<Product | null> {
return this.prisma.product.findUnique({ where: { id } });
}
async createProduct(data: Product): Promise<Product> {
return this.prisma.product.create({ data });
}
async updateProduct(id: number, data: Product): Promise<Product> {
return this.prisma.product.update({ where: { id: Number(id) }, data });
}
async deleteProduct(id: number): Promise<Product> {
return this.prisma.product.delete({ where: { id: Number(id) } });
}
}
product.model.ts
import { Prisma } from '@prisma/client';
export class Product implements Prisma.ProductCreateInput {
id: number;
name: string;
description: string;
price: number;
quantity: number;
}
product.controller.ts
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
} from '@nestjs/common';
import { ProductService } from './product.service';
import { Product } from './product.model';
@Controller('product')
export class ProductController {
constructor(private readonly productService: ProductService) {}
@Get()
async getAllProducts(): Promise<Product[]> {
return this.productService.getAllProducts();
}
@Post()
async postProduct(@Body() postData: Product): Promise<Product> {
return this.productService.createProduct(postData);
}
@Get(':id')
async getProduct(@Param('id') id: number): Promise<Product> {
return this.productService.getProduct(Number(id));
}
@Delete(':id')
async deleteProduct(@Param('id') id: number): Promise<Product> {
return this.productService.deleteProduct(id);
}
@Put(':id')
async updateProduct(
@Param('id') id: number,
@Body() postData: Product,
): Promise<Product> {
return this.productService.updateProduct(id, postData);
}
}
product.module.ts
import { Module } from '@nestjs/common';
import { ProductService } from './product.service';
import { ProductController } from './product.controller';
import { PrismaService } from 'src/prisma.service';
@Module({
providers: [ProductService, PrismaService],
controllers: [ProductController],
})
export class ProductModule {}
เสร็จแล้วเราก็แวะไป import และเรียกใช้ ProductModule ในไฟล์ app.module.ts แล้วรันด้วยคำสั่ง npm run start:dev ได้เลย
จากนั้นเราก็สามารถยิง Postman ทดสอบได้เลย
เมื่อเราเปิด pgAdmin มาก็จะเห็นว่ามีข้อมูลถูกเพิ่มไปแล้ว
การแพ็ค NestJS เป็น Docker Image
ทำการสร้าง Dockerfile สำหรับแพ็คแอปพลิเคชัน NestJS ให้เป็น Docker Image
หลังจากนั้นเพิ่มส่วนในการ build และรัน Dockerfile นี้ไปใน docker-compose.yml เดียวกับฐานข้อมูล PostgreSQL โดยจะมีการต่อ network ตัวเดียวกันให้ nest-app กับ postgreSQL สามารถคุยกันได้
ต่อมาเราก็ใช้คำสั่ง docker compose up -d –build เพื่อให้สิ่งที่เราเขียนในไฟล์ docker compose ทำงานแล้ว build image ใหม่ด้วย
เมื่อลองรันดูแล้วเราจะเห็นว่าตัวแอปไม่ติด เพราะไม่สามารถเชื่อมต่อฐานข้อมูลได้ ให้เราไปเปลี่ยน database connection จาก localhost เป็นชื่อ container ของฐานข้อมูลแทน
เดิม
แก้ใหม่
แล้วลองทำการ docker compose up -d –build ใหม่ และทำการ docker ps ออกมาจะเห็นว่าตอนนี้ทั้งแอปและฐานข้อมูลถูกรันอยู่แล้ว
แล้วถ้าเกิดเรายิง Postman ไปใหม่จะเห็นว่ามันใช้ได้แล้วนั่นเองงง