Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/washira/nest-mongo
A project starter using Nest.js and MongoDB
https://github.com/washira/nest-mongo
mongodb mongoose nestjs
Last synced: about 6 hours ago
JSON representation
A project starter using Nest.js and MongoDB
- Host: GitHub
- URL: https://github.com/washira/nest-mongo
- Owner: Washira
- Created: 2024-03-14T02:30:05.000Z (8 months ago)
- Default Branch: main
- Last Pushed: 2024-06-06T15:17:01.000Z (6 months ago)
- Last Synced: 2024-06-06T17:02:54.485Z (6 months ago)
- Topics: mongodb, mongoose, nestjs
- Language: TypeScript
- Homepage:
- Size: 832 KB
- Stars: 0
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# NestJS and MongoDB
- [NestJS and MongoDB](#nestjs-and-mongodb)
- [Description](#description)
- [Installation](#installation)
- [Running the app](#running-the-app)
- [Test](#test)
- [Support](#support)
- [ER Diagram](#er-diagram)
- [Directory Structure](#directory-structure)
- [Mongo](#mongo)
- [CRUD Operations](#crud-operations)
- [Create Dto](#create-dto)
- [Schema](#schema)
- [Add into `products.module.ts`](#add-into-productsmodulets)
- [Add into `products.service.ts`](#add-into-productsservicets)
- [Validation](#validation)
- [Use more than one model](#use-more-than-one-model)
- [Using `Product` service from `Order` service](#using-product-service-from-order-service)Description
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
Installation
## Installation
```bash
$ npm install
```## Running the app
```bash
# development
$ npm run start# watch mode
$ npm run start:dev# production mode
$ npm run start:prod
```
Test
## Test
```bash
# unit tests
$ npm run test# e2e tests
$ npm run test:e2e# test coverage
$ npm run test:cov
```
Support
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## ER Diagram
```mermaid
erDiagram
MODULE {
name string
imports string[]
controller string[]
providers string[]
exports string[]
}APPLICATION ||--o{ MODULE : contains
MODULE ||--o{ MODULE : imports
MODULE ||--o{ CONTROLLER : defines
CONTROLLER ||--o{ SERVICE : uses
SERVICE ||--|| PROVIDER : implements
SERVICE ||--|| PROVIDER : implements
MODULE ||--|{ PROVIDER : provides
SERVICE }|--|{ DATABASE : interacts-with
```## Directory Structure
เมื่อสร้างโปรเจค NestJS จะได้โครงสร้างไฟล์ดังนี้
```bash
├── 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
```การสร้างโครงสร้างไฟล์เพิ่มเติม
เราสามารถสร้างไฟล์โดยใช้คำสั่ง `nest generate` หรือ `nest g` โดยตัวอย่างเช่น
```bash
nest generate controller
nest generate service
nest generate module
nest generate provider
```หรือต้องการสร้างทีเดียว ได้ไฟล์ทั้งหมด เรียกว่าการสร้าง CRUD resource
โดยใช้คำสั่ง `nest generate resource` หรือ `nest g resource` โดยตัวอย่างเช่น```bash
nest g resource
```## Mongo
ติดตั้ง mongoose
```bash
npm i @nestjs/mongoose mongoose
```เพิ่ม `docker-compose.yml` สำหรับ MongoDB
```bash
touch docker-compose.yml
```คำสั่งในการ execute docker-compose.yml
```bash
docker-compose up -d
```คำสั่งในการ stop docker-compose.yml
```bash
docker-compose down
```คำสั่งในการเช็ค container ที่กำลังทำงาน
```bash
docker ps
```คำสั่งในการเช็ค container ที่เคยทำงาน
```bash
docker ps -a
```คำสั่งในการเข้า container
```bash
docker exec -it bash
```คำสั่งในการเข้า mongo shell
```bash
mongosh -u -p
```คำสั่งในการออกจาก mongo shell หรือ container
```bash
exit
```เพิ่ม `app.module.ts` สำหรับเชื่อมต่อ MongoDB
```typescript
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { ProductsModule } from './products/products.module';@Module({
imports: [
ProductsModule,
MongooseModule.forRoot('mongodb://localhost/nest'),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
```
## CRUD Operationsเริ่มจากการทำ Dto ก่อน
## Create Dto
`Dto` คือ Data Transfer Object เป็นตัวแทนข้อมูลที่ใช้ส่งไปมาระหว่าง client และ server
ในไฟล์ `create-product.dto.ts` ให้กำหนด object ที่ใช้เป็น request body สำหรับ create product
```typescript
export class CreateProductDto {
readonly name: string;
readonly description?: string;
readonly price: number;
}
````?` คือ optional หมายถึงไม่จำเป็นต้องใส่ข้อมูลก็ได้
### Schema
`Entity` คือ object ที่ใช้เป็น model ของ SQL database มักจะใช้งานร่วมกับ TypeORM หรือ Sequelize
ส่วน `Schema` คือ object ที่ใช้เป็น model ของ NoSQL database มักจะใช้งานร่วมกับ Mongooseเนื่องจากเราใช้ MongoDB ดังนั้นเราจะใช้ mongoose ในการกำหนด schema ของ product
จึงไม่ใช้ Entity แต่ใช้ Schema แทน
สร้าง `schema` folder สำหรับ products module ซึ่งภายใน ให่้สร้างไฟล์ `product.schema.ts`
ในไฟล์ `product.schema.ts` ให้กำหนด schema ของ product
```typescript
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);
```จะมีการ export `Product`, `ProductDocument` และ `ProductSchema` ออกไปใช้งาน
### Add into `products.module.ts`
เพิ่ม `MongooseModule.forFeature` ใน `products.module.ts` สำหรับเชื่อมต่อ schema ของ product
จะมีการ import `Product` และ `ProductSchema` มาใช้งาน
```typescript
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { ProductsController } from './products.controller';
import { ProductsService } from './products.service';
+ import { Product, ProductSchema } from './schemas/product.schema';@Module({
+ imports: [
+ MongooseModule.forFeature([{ name: Product.name, schema: ProductSchema }]),
+ ],
controllers: [ProductsController],
providers: [ProductsService],
})
export class ProductsModule {}
````forFeature` คือ method ที่ใช้เชื่อมต่อ schema ของ product กับ MongooseModule
ภายในมี object ที่มี key คือ `name` และ `schema` โดย- `name` คือชื่อของ schema
- `schema` คือ schema ของ product### Add into `products.service.ts`
ในส่วนของ `product.schema.ts` จะมีการ import `Product` และ `ProductDocument` มาใช้งาน
ในส่วนของ `@nestjs/mongoose` จะมีการ import `InjectModel`
ในส่วนของ `mongoose` จะมีการ import `Model` และ `InjectModel`
```typescript
// products.service.ts
import { Body, Controller, Get, Post } from '@nestjs/common';
import { CreateProductDto } from './dto/create-product.dto';
import { ProductsService } from './products.service';
+ import { Product, ProductDocument } from './schemas/product.schema';
+ import { InjectModel } from '@nestjs/mongoose';
+ import { Model } from 'mongoose'
```ต่อมา สร้าง constructor ใน `ProductsService` และใช้ `@InjectModel` ในการ inject dependencies จาก `mongoose`
ในที่นี้คือ `Product`
และประกาศตัวแปรที่จะเรียกใช้งานใน `ProductsService` คือ `productModel`
ซึ่งมี concept คล้ายๆกับ controller```typescript
@Injectable()
export class ProductsService {
+ constructor(@InjectModel(Product.name) private productModel: Model) {}
...
}
```ลองเรียกใช้ `productModel` ใน `ProductsService` ด้วยการเพิ่ม expression สำหรับ create product
```typescript
@Injectable()
export class ProductsService {
constructor(@InjectModel(Product.name) private productModel: Model) {}+ async create(createProductDto: CreateProductDto): Promise {
+ const createdProduct = new this.productModel(createProductDto);
+ return createdProduct.save();
+ }
...
}
```เนื่องจาก `createdProduct.save()` เป็น promise จึงให้ใส่ `async` และ `await` ในการรอให้ promise สำเร็จ
ความเจ๋งของ `Next.js` คือ เมื่อมีการระบุ `async` ใน method ของ service แล้ว การเรียกใช้งานในฝั่ง controller ไม่ต้องระบุ `async` และ `await` อีก
เมื่อลอง request POST ผ่าน Postman จะได้ response ประมาณนี้
```json
{
"_id": "60f3e3e3e3e3e3e3e3e3e3e3",
"name": "Product 1",
"description": "Description 1",
"price": 1000,
"__v": 0
}
```ก็ถือว่าการติดตั้ง Mongoose ใน CRUD สำเร็จแล้ว
## Validation
ในบางครั้งที่เราอาจจะส่ง request body ที่ไม่ถูกต้อง ซึ่งอาจจะทำให้เกิด error ในการบันทึกข้อมูลลงใน database
และเมื่อเกิด error จะได้ response กลับไปว่า Internal server error ประมาณนี้```json
{
"statusCode": 500,
"message": "Internal server error"
}
```ปัญหาคือ เราไม่ทราบว่า field ไหนที่ส่งข้อมูลไม่ถูกต้อง ถ้าเราต้องการรายละเอียดของ error จะต้องทำการ validate request body ก่อน
เราสามารถใช้ `class-validator` ในการ validate request body ของ create product
ติดตั้ง `class-validator` และ `class-transformer`
```bash
npm i class-validator class-transformer
```เพิ่ม `class-validator` ใน `create-product.dto.ts`
```typescript
+ import { IsString, IsNumber, IsOptional, IsNotEmpty } from 'class-validator';export class CreateProductDto {
+ @IsNotEmpty()
+ @IsString()
readonly name: string;+ @IsOptional()
+ @IsString()
readonly description?: string;+ @IsNotEmpty()
+ @IsNumber()
readonly price: number;
}
```แต่ `class-validator` ไม่ได้มากับ `Nest.js` จึงต้องเพิ่ม `class-validator` ใน `main.ts` ด้วย
```typescript
+ import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';async function bootstrap() {
const app = await NestFactory.create(AppModule);
+ app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
```เมื่อลอง request POST หรือ PATCH ผ่าน Postman ด้วย request body `price` ที่ไม่ถูกต้อง จะได้ response ประมาณนี้
```json
{
"message": [
"price must be a number conforming to the specified constraints"
],
"error": "Bad Request",
"statusCode": 400
}
```
## Use more than one modelในการใช้งานจริง มีโอกาสที่เราจะใช้งานมากกว่า 1 model ใน module เดียวกัน ได้
ในที่นี้ ยกตัวอย่างการสร้างอีก 1 model คือ `Order` เพื่อทำงานร่วมกับ `Product`
มีการสร้าง resource เหมือนกันตอนที่สร้าง `Product` แต่ `schemas` ของ `Order` จะมีแค่ `productId` และ `quantity` เท่านั้น
ใน `order.dto.ts` ให้กำหนด object ที่ใช้เป็น request body สำหรับ create order
```typescript
import { IsNumber, IsNotEmpty, IsMongoId, Min } from 'class-validator';export class CreateOrderDto {
@IsMongoId()
@IsNotEmpty()
readonly productId: string;@IsNumber()
@Min(1)
readonly quantity: number;
}
```เพิ่ม `Order` และ `OrderSchema` ใน `products.module.ts`
```typescript
...
+ import { Order, OrderSchema } from './schemas/order.schema';@Module({
imports: [
+ MongooseModule.forFeature([{ name: Order.name, schema: OrderSchema }]),
],
...
})
...
```เพิ่มเหมือนกันกับตอนที่ทำ `Product` ทุกอย่าง
แล้วก็ตั้งค่า controller และ service ของ `Order` โดยใช้แบบตาม `Product` ก็จะได้ CRUD ของ `Order` แล้ว
การสร้าง `Order` จะใช้ `productId` ของ `Product` ที่มีอยู่แล้ว และ `quantity` ที่ต้องการสั่งซื้อ เป็นการเรียกข้อมูลจาก `Product` มาใช้งานใน `Order`
## Using `Product` service from `Order` service
ต่อมา เราจะปรับปรุง `Order` service ให้เช็ค `productId` ว่ามีอยู่จริงใน `Product` หรือไม่ ถ้าไม่ ให้ส่ง error กลับไปว่า `Product not found`
เราสามารถใช้งาน service ของ `Product` จาก `Order` ได้ เพื่อเรียก method ของ `Product` มาใช้งานใน `Order`
เริ่มต้นจาก การ export `ProductService` ใน `products.module.ts`
```typescript
...
@Module({
...
exports: [ProductsService],
})
```จากนั้น ใน `orders.module.ts` ให้ import `ProductsModule`
```typescript
...
+ import { ProductsModule } from '../products/products.module';@Module({
imports: [
...
+ ProductsModule,
],
...
})
```
เป็นการ import module สู่ module อื่น เป็นการใช้ประโยชน์จากการใช้งาน module ใน NestJS
ที่ import เข้ามาทั้ง module เพื่อให้ `Order` service สามารถใช้งาน `Product` service ได้ โดยไม่ต้อง import `ProductService`, `ProductModel` หรืออื่นๆ แยกกันต่อมา ใน `orders.service.ts` ให้ import `ProductsService` และใช้งาน method ของ `ProductsService` ในการเช็ค `productId` ว่ามีอยู่จริงใน `Product` หรือไม่
```typescript
...
+ import { ProductsService } from '../products/products.service';@Injectable()
export class OrdersService {
constructor(
@InjectModel(Order.name) private orderModel: Model,
+ private productsService: ProductsService,
) {}async create(createOrderDto: CreateOrderDto): Promise {
+ const product = await this.productsService.findOne(createOrderDto.productId);
+ if (!product) {
+ throw new NotFoundException('Product not found');
+ }
const createdOrder = new this.orderModel(createOrderDto);
return createdOrder.save();
}
...
}
```เมื่อลอง request POST หรือ PATCH ผ่าน Postman ด้วย `productId` ที่ไม่มีอยู่จริง จะได้ response ประมาณนี้
```json
{
"statusCode": 404,
"message": "Product not found",
"error": "Not Found"
}
```