Bài viết có follow techniques từ Nestjs, cộng với áp dụng vài best practices, ngay ở trang Mongodb techniques từ Nestjs cũng đã đề cập ta có nhiều cách để setup database: dùng ORM hoặc dùng Mongoose, mình sẽ trình bày cách setup Mongodb phố biến nhất là dùng Mongoose ở đây thôi.
Nếu bạn đang vội thì đây là folder structure cuối cùng, với source code hoàn chỉnh được đặt trong repo này của mình: simple-nestjs-boilerplate
📂 Folder Structure
Còn dưới đây là chi tiết triển khai cho các bạn sống chậm. Nào bắt đầu thôi!
Configuration
Muốn kết nối được với database thì đầu tiên ta cung cấp các thông tin cần thiết về credentials dùng để kết nối với database đó, thường là chúng ta sẽ đặt các thông tin đó trong biến môi trường env, ở đây mình có file configuration.ts mục đích là load các biến môi trường cần thiết và nhóm các biến có cùng mục đích lại với nhau.
export default () => ({
port: parseInt(process.env.PORT, 10) || 3000,
database: {
uri: process.env.DB_URI,
name: process.env.DB_NAME,
pass: process.env.DB_PASS,
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '1h',
},
});
Rồi giờ để dùng config bên trên cho việc kết nối database thì mình có file mongoose-config.service.ts
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import type {
MongooseModuleOptions,
MongooseOptionsFactory,
} from '@nestjs/mongoose';
@Injectable()
export class MongooseConfigService implements MongooseOptionsFactory {
constructor(private configService: ConfigService) {}
createMongooseOptions(): MongooseModuleOptions {
return {
uri: this.configService.get<string>('database.uri'),
dbName: this.configService.get<string>('database.name'),
user: this.configService.get<string>('database.user'),
pass: this.configService.get<string>('database.pass'),
};
}
}
Bạn có thể thấy thằng MongooseConfigService có inject thằng ConfigService, điều đó có nghĩa là bất cứ thằng nào sử dụng MongooseConfigService đều sẽ phải import thằng ConfigModule vào. Đó là lý do mình tạo ra CoreModule để cho 2 thằng trên vào 1 module - vì đằng nào 2 thằng này đi đâu cũng phải có nhau, và còn mục đích khác mà mình sẽ trình bày bên dưới.
import { Global, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { MongooseModule } from '@nestjs/mongoose';
import { MongooseConfigService } from '../database/mongoose-config.service';
import configuration from '../config/configuration';
/**
* CoreModule encapsulates essential application infrastructure configurations.
* It sets up the global ConfigModule and the primary Mongoose database connection.
* By marking it as @Global(), its providers (like ConfigService) and exports
* (like MongooseModule connection) are available application-wide without needing
* to import CoreModule into every feature module.
*/
@Global() // Make ConfigService and Mongoose connection available globally
@Module({
imports: [
ConfigModule.forRoot({
load: [configuration],
isGlobal: true, // Keep this true for easy access to ConfigService everywhere
}),
MongooseModule.forRootAsync({
useClass: MongooseConfigService,
}),
],
// No providers needed here unless CoreModule itself has specific services.
// No need to export ConfigModule as it's global.
// MongooseModule.forRootAsync handles the connection globally.
// Feature modules will use MongooseModule.forFeature() to get specific models.
exports: [
// We don't strictly need to export MongooseModule here because forRootAsync
// establishes the connection globally. Feature modules use forFeature.
// However, exporting it can sometimes be useful for clarity or specific scenarios,
// but it's generally not required for the standard pattern.
// MongooseModule
],
})
export class CoreModule {}
Giờ thì ta chỉ cần import CoreModule bên trên vào app.module.ts là có thể hoàn tất bước kết nối database rồi.
import { Module } from '@nestjs/common';
import { CoreModule } from './core/core.module';
@Module({
imports: [CoreModule],
})
export class AppModule {}
Schema đầu tiên của bạn
Kết nối xong xuôi rồi giờ là lúc ta bàn đến cách làm sao để tương tác với database: thêm, sửa và xóa dữ liệu. Well, ta sẽ tương tác thông qua những models và schemas chính là nơi ta định nghĩa hình dáng của các models.
Ở đây mình có schema ko thể đơn giản hơn cho user model:
import { Prop, Schema, SchemaFactory } from "@nestjs/mongoose";
import { HydratedDocument } from 'mongoose';
export type UserDocument = HydratedDocument<User>;
@Schema()
export class User {
@Prop()
email: string;
@Prop()
password: string;
}
export const UserSchema = SchemaFactory.createForClass(User);
Rồi làm sao để sử dụng cái schema trên để tương tác với database? Giả sử trong trường hợp ta cần authenticate một user, người dùng sẽ gửi lên email và password và ta sẽ phải query trong database để kiểm tra xem email và password có hợp lệ hay ko. Vậy thì thằng sử dụng UserSchema trong trường hợp này là AuthModule nên mình cần import schema vào auth.module.ts để sử dụng.
import { Module } from '@nestjs/common';
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from '../database/schemas/user.schema';
@Module({
imports: [
MongooseModule.forFeature([
{
name: User.name,
schema: UserSchema,
},
]),
],
// providers: [AuthService],
// controllers: [AuthController],
})
export class AuthModule {}
Khi mà ta đã import schema vào trong thằng AuthModule rồi thi thằng service bên trong module đó sẽ có thể inject cái model tương ứng để sử dụng, nhớ rằng ta khai báo schema nào trong thằng AuthModule thì chỉ được sử dụng model liên quan đến schema ấy trong AuthService.
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { InjectModel } from '@nestjs/mongoose';
import type { Model } from 'mongoose';
import { User } from '../database/schemas/user.schema';
import type { AuthLoginDto } from './dto/auth-login.dto';
@Injectable()
export class AuthService {
constructor(
@InjectModel(User.name) private userModel: Model<User>,
private jwtService: JwtService,
) {}
async login(loginDto: AuthLoginDto) {
const { email, password } = loginDto;
// do validate user input here
const foundUser = await this.userModel.findOne({ email });
if (foundUser.password !== password) throw new UnauthorizedException();
const payload = { email, id: foundUser.id };
return {
token: this.jwtService.sign(payload),
};
}
}
warning
Bài viết chỉ tập trung vào setup database, chứ ko bàn sâu về các khía cạnh kỹ thuật khác. Tất nhiên trong ứng dụng thực tế (real world application) ta ko bao giờ lưu raw password cả, mà nó phải được hash trước khi lưu vào db.
🌱 Gieo mầm database (Seed data)
Để gieo mầm chuẩn chỉ thì ta theo các bước dưới đây:
1. Tạo Seed Data Files
Store seed data in TypeScript files for type safety and easy imports:
export const usersSeed = [
{
name: 'Admin User',
email: 'admin@example.com',
}
];
2. Tạo Seed Service
Thường thì ta hay seed data lần đầu tiên khi khởi chạy app thôi, nên mình ko handle case bị duplicate dữ liệu ở đây, nếu bạn phải seed nhiều lần thì nên để ý vấn đề đó.
import { Injectable, Logger } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model } from 'mongoose';
import { User, UserDocument } from '../database/schema/user.schema';
import { usersSeed } from './data/users.seed';
@Injectable()
export class SeedService {
private readonly logger = new Logger(SeedService.name);
constructor(@InjectModel(User.name) private userModel: Model<UserDocument>) {}
async seed() {
await this.seedUsers();
}
async seedUsers() {
const users = await this.userModel.create(usersSeed);
this.logger.log(`Seeded ${users.length} users`);
}
}
3. Tạo Seed Module
import { Module } from '@nestjs/common';
import { SeedService } from './seed.service';
import { MongooseModule } from '@nestjs/mongoose';
import { User, UserSchema } from '../database/schemas/user.schema';
import { CoreModule } from '../core/core.module';
@Module({
imports: [
CoreModule,
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
],
providers: [SeedService],
})
export class SeedModule {}
Nhớ đoạn setup configuration tại sao mình lại tách ra CoreModule riêng ko, đây là vì nó sẽ được dùng ở thằng SeedModule này, thằng SeedModule sẽ được chạy ngoài thằng AppModule nên register CoreModule globally ko có tác dụng ở đây, bạn vẫn phải import nó ở đây.
4. Tạo Seed Script
import { NestFactory } from '@nestjs/core';
import { SeedModule } from '../seed/seed.module';
import { SeedService } from '../seed/seed.service';
async function bootstrap() {
try {
const app = await NestFactory.createApplicationContext(SeedModule);
const seedService = app.get(SeedService);
await seedService.seed();
await app.close();
process.exit(0);
} catch (error) {
console.error('Seed failed:', error);
process.exit(1);
}
}
bootstrap();
5. Update Package.json
{
"scripts": {
"seed": "ts-node -r tsconfig-paths/register src/scripts/seed.ts",
"bun:seed": "bun src/scripts/seed.ts"
}
}
Chạy seeding thôi:
Best Practices khi "gieo mầm"
- Environment Checks
Validate biến môi trường ở seed service:
if (process.env.NODE_ENV !== 'development') {
throw new Error('Seeding only allowed in development environment');
}
- Transactional Seeding Sử MongoDB transactions cho nhất quán dữ liệu:
async seedUsers() {
const session = await this.connection.startSession();
session.startTransaction();
try {
// Seed operations
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
throw error;
} finally {
session.endSession();
}
}
- Data Validation
Sử dụng Mongoose validation khi insert dữ liệu:
const users = usersSeed.map(user => new this.userModel(user));
await this.userModel.insertMany(users);
- Dependency Order
Chú ý thứ tự khi seeding.
async seed() {
await this.seedRoles();
await this.seedUsers(); // Users phụ thuộc vào roles nên chạy sau
}