Mình thì xuất thân một Dev PHP laravel, trong thời gian vừa qua mình có tham gia dự án nhưng khách hàng muốn sử dụng Oracle SQL. Còn Laravel support Oracle không thực sự tốt và có rất nhiều rủi nên các anh trong dự án mình đưa ra quyết đinh “quay xe” sang Nodejs cụ thể là Nestjs. Mình cũng đồng ý luôn vì chưa biết thì học cũng rất là tốt, và đến thời điểm hiện tại thì việc để chúng ta học thêm một ngôn ngữ mới và tiếp cận nó không còn quá khó khăn. Nên trong bài viết này mình sẽ chia sẻ lại những kiến thức mình đã tích lũy được và xây dựng Repository basic + TypeOrm như của đối thủ cạnh tranh của Nest là Laravel/PHP.
Bài viết dừng lại ở việc chia sẻ kiến thức của mình và mình cũng chưa có nhiều thời gian làm việc với nó, nên rất mong mọi người đóng góp và ủng hộ
Mình cũng sẽ xây dựng một series về nestjs để các bạn muốn tìm hiểu và học có thể tham khảo. Còn bây giờ bắt đầu nhé 👇
1. Giới thiệu
Nestjs là một framework được xây dựng nên các ứng dụng phía Server-side và chạy bằng Nodejs, ngoài Nestjs thì còn Hapi.js, Express.js, Koa.js… cũng là frameword của Nodejs.
Nest cung cấp một kiến trúc ứng dụng out-of-the-box cho phép các developer và nhóm tạo ra các ứng dụng có thể test, có thể mở rộng, móc nối và dễ bảo trì. Kiến trúc được lấy cảm hứng từ Angular.
2. Cài đặt
- Trước tên chúng ta cần install các packege sau:
> npm i -g @nestjs/cli
> nest newproject-name
> npm i --save @nestjs/core @nestjs/common rxjs reflect-metadata
> npm i --save @nestjs/typeorm typeorm mysql
> npm install -g ts-node
> npm install class-transformer
- Tiếp đến là folder structure của dự án:
src/ ├── databse/| ├── migrations/ │ │ └── 123456789-messageTable.ts └── models/ ├── messages/| ├── interfaces/ │ │ └── message.interface.ts | ├── entities/ │ │ └── message.entity.ts | ├── serializers/ │ │ └── message.serializer.ts │ ├── messages.controller.ts │ ├── messages.module.ts │ ├── messages.repository.ts │ └── messages.service.ts ├── model.repository.ts ├── model.serializer.ts └── orm-config.ts
- model.repository.ts sẽ là basic repository của dự án.
- Để có thể có thể connect đến mysql và migrate thì cần config một số thứ sau trong file orm-config.tss
import{ MysqlConnectionOptions }from'typeorm/driver/mysql/MysqlConnectionOptions';const config: MysqlConnectionOptions ={
type:'mysql',
database:'nest',
username:'root',
password:'root',
port:3306,
host:'127.0.0.1',
entities:['dist/models/**/*.entity{.ts,.js}'],
synchronize:false,// false để khi bạn thay đổi trong entities nó sẽ không tự update DB
dropSchema:false,
migrations:["dist/database/migrations/*.js",],
cli:{
migrationsDir:'src/database/migrations'// migrate file sẽ được sinh ra tại đây}}exportdefault config;
- Sau đó là app.module.ts
import{ Module }from'@nestjs/common';import{ AppController }from'./app.controller';import{ AppService }from'./app.service';import{ TypeOrmModule }from'@nestjs/typeorm';import config from'./orm-config';import{ MessagesModule }from'./models/messages/messages.module';
@Module({
imports:[
TypeOrmModule.forRoot(config),
MessagesModule,],
controllers:[AppController],
providers:[AppService],})exportclassAppModule{}exportdefault config;
- Config để generate migrate, run migrate … cần thiết lập trong package.json
"scripts":{"typeorm":"node --require ts-node/register ./node_modules/typeorm/cli.js --config src/orm-config.ts","migration":"yarn typeorm migration:run","migration:create":"yarn typeorm migration:create -n","migration:revert":"yarn typeorm migration:revert",.....}exportdefault config;
- Run migrate:
> yarn run migration //chạy các file migrate> yarn run migration:create MessageTable //generate file migrate> yarn run migration:revert //migrate rollback
3. Setting Basic Repository
- models/model.repository.ts
import{ Injectable, NotFoundException }from'@nestjs/common';import{ plainToClass }from'class-transformer';import{ ModelEntity }from'./model.serializer';import{ DeepPartial, Repository }from'typeorm'; @Injectable()exportclassModelRepository<T,KextendsModelEntity>extendsRepository<T>{asyncgetAllEntity( relations: string[]=[], throwsException =false): Promise<K[]|null>{returnawaitthis.find({relations}).then(entity=>{if(!entity && throwsException){return Promise.reject(newNotFoundException('Model not found'))}return Promise.resolve(entity ?this.transformMany(entity):null)})}asyncgetEntityById( id: string | number, relations: string[]=[], throwsException =false): Promise<K|null>{returnawaitthis.findOne({ where:{ id }, relations }).then(entity=>{if(!entity && throwsException){return Promise.reject(newNotFoundException('Model not found'))}return Promise.resolve(entity ?this.transform(entity):null)})}asynccreateEntity( inputs: DeepPartial<T>, relations: string[]=[]): Promise<K>{returnawaitthis.save(inputs).then(asyncentity=>{returnawaitthis.getEntityById((entity as any).id, relations)}).catch(error=> Promise.reject(error))}asyncupdateEntity( entity:K, inputs: DeepPartial<T>, relations: string[]=[]): Promise<K>{returnawaitthis.update(entity.id, inputs).then(asyncentity=>{returnawaitthis.getEntityById((entity as any).id, relations)}).catch(error=> Promise.reject(error))}asyncdeleteEntityById( id: number | string,): Promise<boolean>{returnawaitthis.delete(id).then(()=>{returntrue}).catch(error=> Promise.reject(error))}transform(model:T, transformOptions ={}):K{returnplainToClass(ModelEntity, model, transformOptions)asK;}transformMany( model:T[], transformOptions ={}):K[]{return model.map(model=>this.transform(model, transformOptions))}}
- Trong đó T : Đại diện cho Model, K: Đại diện cho Interface
- Ngoài ra, hãy nhớ rằng đây chỉ là một số chức năng. Bạn có thể tạo bao nhiêu chức năng repo khác nếu bạn muốn ( ví dụ. getWhere, destroyByEmail….
).
- model.serializer.tss
exportclassModelEntity{
id: number | string;[key: string]: any;}
4. Setting For Message Model
- Đầu tiên mình sẽ đi vào messages/interfaces/message.interface.ts nơi định nghĩa các field của User
exportinterfaceIMessage{
id: number | string,
conversation_id: number |null,
status: boolean,
message: string |null,}
- Thiếp lập migration file 123456789-messageTable.ts (nhớ là file này sinh ra từ câu lệnh generate bên trên)
import{MigrationInterface, QueryRunner, Table}from"typeorm";exportclassmessageTable1632326169350implementsMigrationInterface{publicasyncup(queryRunner: QueryRunner): Promise<void>{await queryRunner.createTable(newTable({
name:'messages',
columns:[{
name:'id',
type:'bigint',
isPrimary:true,
isGenerated:true,
generationStrategy:'increment',},{
name:'conversation_id',
type:'bigint',
isNullable:true},{
name:'status',
type:'boolean',
isNullable:true},{
name:'message',
type:'varchar',
isNullable:true},{
name:'created_at',
type:'timestamp',
isNullable:true,default:'now()'},{
name:'updated_at',
type:'timestamp',
isNullable:true,default:'now()'}],}))}publicasyncdown(queryRunner: QueryRunner): Promise<void>{await queryRunner.dropTable('messages')}}
- messages/entities/message.entity.ts
import{ Column }from'typeorm'import{ IMessage }from'../interfaces/message.interface'import{
Entity,
PrimaryGeneratedColumn,
CreateDateColumn,
UpdateDateColumn
}from'typeorm';
@Entity({ name:'messages'})exportclassMessageimplementsIMessage{
@PrimaryGeneratedColumn()
id: string;
@Column({name:'conversation_id', nullable:true})
conversation_id: number
@Column({default:true})
status: boolean
@Column({name:'message', length:255})
message: string
@CreateDateColumn({ name:'created_at', type:'timestamp', nullable:true})
createdAt: Date;
@UpdateDateColumn({ name:'updated_at', type:'timestamp', nullable:true})
updatedAt: Date;}
- messages/serializers/message.serializer.ts
import{ IMessage }from'../interfaces/message.interface';import{ Expose }from'class-transformer';import{ ModelEntity }from'../../model.serializer';exportconst defaultMessageGroupsForSerializing: string[]=['message.timestamps'];exportconst extendedMessageGroupsForSerializing: string[]=[...defaultMessageGroupsForSerializing,];exportconst allMessageGroupsForSerializing: string[]=[...extendedMessageGroupsForSerializing,'message.conversation_id',];exportclassMessageEntityextendsModelEntityimplementsIMessage{
id: number | string
conversation_id:null| number;
status: boolean;
message: string |null;
@Expose({ groups:['message.timestamps']})
createdAt: Date;
@Expose({ groups:['message.timestamps']})
updatedAt: Date;}
- Tiếp đó là messages/message.repository.ts, ở đây ta sẽ tiến hành overwrite lại các function của Basic repository
import{ EntityRepository }from'typeorm';import{ Message }from'./entities/message.entity'import{ ModelRepository }from'../model.repository';import{ allMessageGroupsForSerializing, MessageEntity }from'./serializers/message.serializer';import{ plainToClass, classToPlain }from'class-transformer';
@EntityRepository(Message)exportclassMessagesRepositoryextendsModelRepository<Message, MessageEntity>{transform(model: Message): MessageEntity {const transformOptions ={
groups: allMessageGroupsForSerializing
}returnplainToClass(
MessageEntity,classToPlain(model, transformOptions),
transformOptions
)}transformMany(models: Message[]): MessageEntity[]{return models.map(model=>this.transform(model));}}
- Muốn control được luồng thì chúng ta cần phải có controller đúng không messages/message.controller.ts
import{
Get, Put, Post,Body, Delete,
Param, Controller, UseInterceptors, SerializeOptions, ClassSerializerInterceptor, HttpException, HttpStatus,}from'@nestjs/common';import{
extendedMessageGroupsForSerializing,
MessageEntity,}from'./serializers/message.serializer';import{MessagesService}from'./messages.service';import{ Message }from'./entities/message.entity';
@Controller('messages')
@SerializeOptions({
groups: extendedMessageGroupsForSerializing,})exportclassMessagesController{constructor(private readonly messageService: MessagesService){}
@Get('/')
@UseInterceptors(ClassSerializerInterceptor)asyncindex(){returnthis.messageService.findAll()}
@Get('/:id')
@UseInterceptors(ClassSerializerInterceptor)asyncgetById(
@Param() params
): Promise<MessageEntity>{const message =awaitthis.messageService.findById(params.id);this.throwMessageNotFound(message)return message
}
@Post('/')
@UseInterceptors(ClassSerializerInterceptor)asynccreate(
@Body() inputs: Message,): Promise<MessageEntity>{returnawaitthis.messageService.create(inputs);}
@Put('/:id')
@UseInterceptors(ClassSerializerInterceptor)asyncupdate(
@Param() params,
@Body() inputs: Message,): Promise<MessageEntity>{const message =awaitthis.messageService.findById(parseInt(params.id,0))this.throwMessageNotFound(message)returnawaitthis.messageService.update(message, inputs);}
@Delete('/:id')asyncdelete(
@Param() params,): Promise<Boolean>{const message =awaitthis.messageService.findById(parseInt(params.id,0))this.throwMessageNotFound(message)returnawaitthis.messageService.deleteById(params.id);}throwMessageNotFound(message: MessageEntity){if(!message){thrownewHttpException('Not found', HttpStatus.NOT_FOUND)}}}
- Tiếp sẽ là thiết lập service -> repository messages/messages.service.ts
import{Injectable}from'@nestjs/common';import{ InjectRepository }from'@nestjs/typeorm';import{ MessagesRepository }from'./messages.repository';import{ MessageEntity }from'./serializers/message.serializer';import{ Message }from'./entities/message.entity';
@Injectable()exportclassMessagesService{constructor(@InjectRepository(MessagesRepository)private usersRepository: MessagesRepository){}asyncfindAll(
relations: string[]=[],
throwsException =false): Promise<MessageEntity []>{returnawaitthis.usersRepository.getAllEntity(relations, throwsException)}asynccreate(
inputs: Message,): Promise<MessageEntity>{returnawaitthis.usersRepository.createEntity(inputs)}asyncfindById(
id: number,
relations: string[]=[],
throwsException =false): Promise<MessageEntity>{returnawaitthis.usersRepository.getEntityById(id, relations, throwsException)}asyncupdate(
user: MessageEntity,
inputs: Message,): Promise<MessageEntity>{returnawaitthis.usersRepository.updateEntity(user, inputs)}asyncdeleteById(id: number): Promise<Boolean>{returnawaitthis.usersRepository.deleteEntityById(id)}}
- Chốt hạ là messages.module.ts
import{ Module, NestModule, MiddlewareConsumer }from'@nestjs/common';import{ MessagesController }from'./messages.controller';import{ MessagesService }from'./messages.service';import{ TypeOrmModule }from'@nestjs/typeorm';import{ MessagesRepository }from'./messages.repository';import{ ConfigModule }from'@nestjs/config';
@Module({
imports:[ConfigModule, TypeOrmModule.forFeature([MessagesRepository])],
controllers:[MessagesController],
providers:[MessagesService, ConfigModule],
exports:[MessagesService],})exportclassMessagesModule{}
5. Kết luận
- Thực sự bài viết cúng khá là dài, nhưng mình đã chia sẻ đây là bài hướng dẫn khá là chi tiết để xây dựng 1 Basic Reposiroy theo luồng Controller -> Service -> Repository -> TypeOrm và rất là chặt chẽ.
- Hy vọng bài viết này đã giúp bạn. Nếu có bất kì thắc mắc hãy liên hệ với mình
- Trong phần tiếp theo mình sẽ giúp các bạn using JWT trong Nest, rất mong được các bạn ủng hộ.
- Bạn cũng có thể tham khảo của mình Repository : Tại đây
Thanks for Reading
Nguồn: viblo.asia