NestJS-Module Nesting

jaegeunsong97·2024년 2월 22일
0

NestJS

목록 보기
33/37

🖊️Module Nesting

댓글 기능을 만들도록 하겠습니다. 댓글의 경우 Post 내부에 존재하기 때문에 posts 내부에 생성을 하도록 하겠습니다.

nest g resource -> comments -> REST API -> n
  • posts/comments/comments.controller.ts
@Controller('posts/:postId/comments')
export class CommentsController {
  constructor(private readonly commentsService: CommentsService) {}

  /**
   * 1) Entity 생성
   * author -> 작성자
   * comment -> 실제 댓글 내용
   * likeCount -> 좋아요 갯수
   * 
   * id -> PrimaryGeneratedColumn
   * createAt -> 생성일자
   * updatedAt -> 업데이트일자
   * 
   * 2) GET() pagination
   * 3) GET(':commentId') 특정 comment만 하나 가져오는 기능
   * 4) POST() 코멘트 생성하는 기능
   * 5) PATCH(':commentId') 특정 comment 업데이트 하는 기능
   * 6) DELETE(':commentId) 특정 comment 삭제하는 기능
   */
}

🖊️Comments Entiy

  • posts/comments/entity/comments.entity.ts
import { IsNumber, IsString } from "class-validator";
import { BaseModel } from "src/common/entity/base.entity";
import { PostsModel } from "src/posts/entity/posts.entity";
import { UsersModel } from "src/users/entity/users.entity";
import { Column, Entity, ManyToOne } from "typeorm";

@Entity()
export class CommentsModel extends BaseModel {
     
    @ManyToOne(() => UsersModel, (user) => user.postComments)
    author: UsersModel;

    @ManyToOne(() => PostsModel, (post) => post.comments)
    post: PostsModel;

    @Column()
    @IsString()
    comment: string;

    @Column({
      	default: 0
    })
    @IsNumber()
    likeCount: number;
}
  • posts.entity.ts
.
.
@OneToMany(() => CommentsModel, (comment) => comment.post)
comments: CommentsModel[];
  • users.entity.ts
.
.
@OneToMany(() => CommentsModel, (comment) => comment.author)
postComments: CommentsModel[];

app.module.ts에 등록을 하고 typeORM 사용을 위해 typeORM 등록을 하겠습니다.

  • app.module.ts
entities: [
    PostsModel,
    UsersModel,
    ImageModel,
    ChatsModel,
    MessagesModel,
    CommentsModel, // 등록
],
  • comments.module.ts
import { Module } from '@nestjs/common';
import { CommentsService } from './comments.service';
import { CommentsController } from './comments.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CommentsModel } from './entity/comments.entity';

@Module({
    imports: [
        TypeOrmModule.forFeature([
          	CommentsModel,
        ])
    ],
    controllers: [CommentsController],
    providers: [CommentsService],
})
export class CommentsModule {}

🖊️Paginate Comments API

  • comments/dto/paginate-comments.dto.ts
import { BasePaginationDto } from "src/common/dto/base-pagination.dto";

export class PaginateCommentsDto extends BasePaginationDto {}
  • comments.controller.ts
@Get()
getComments(
    @Param('postId', ParseIntPipe) postId: number,
    @Query() query: PaginateCommentsDto
) {
    return this.commentsService.paginateComments(
        query,
        postId,
    );
}
  • comments.service.ts
@Injectable()
export class CommentsService {

    constructor(
    	@InjectRepository(CommentsModel)
       	private readonly commentsRepository: Repository<CommentsModel>,
       	private readonly commonService: CommonService,
    ) {}

    paginateComments(
    	dto: PaginateCommentsDto,
     	postId: number,
    ) {
        return this.commonService.paginate(
            dto,
            this.commentsRepository,
            {
                where: {
                    post: {
                      	id: postId,
                    }
                }
            },
            `posts/${postId}/comments`,
        );
    }
}
  • comments.module.ts
@Module({
    imports: [
        TypeOrmModule.forFeature([
          	CommentsModel,
        ]),
        CommonModule, // 등록
    ],
    controllers: [CommentsController],
    providers: [CommentsService],
})
export class CommentsModule {}

🖊️ID 기반으로 하나의 Comment 가져오는 API

  • comments.controller.ts
@Get(':commentId')
getComment(
  	@Param('commentId', ParseIntPipe) commentId: number,
) {
  	return this.commentsService.getCommentById(commentId);
}
  • comments.service.ts
async getCommentById(id: number) {
    const comment = await this.commentsRepository.findOne({
        where: {
          	id,
        }
    });
    if (!comment) throw new BadRequestException(`id: ${id} Comment는 존재하지 않습니다. `);
  	return comment;
}

포스트맨으로 테스트를 하겠습니다.

{
    "message": "id: 1 Comment는 존재하지 않습니다. ",
    "error": "Bad Request",
    "statusCode": 400
}

🖊️Comment 생성 API

  • dto/create-comments.dto.ts
import { PickType } from "@nestjs/mapped-types";
import { CommentsModel } from "../entity/comments.entity";

export class CreateCommentsDto extends PickType(CommentsModel, [
     'comment' // comment 프로퍼티만 상속받기
]) {}
  • comments.controller.ts
@Post()
@UseGuards(AccessTokenGuard)
postComment(
    @Param('postId', ParseIntPipe) postId: number,
    @Body() body: CreateCommentsDto,
    @User() user: UsersModel
) {
    return this.commentsService.createComment(
        body, 
        postId, 
        user
    )
} 
  • comments.service.ts
async createComment(
    dto: CreateCommentsDto,
    postId: number,
    author: UsersModel // AccessToken에서 넘겨주면 UsersModel이 들어있음
) {
    return this.commentsRepository.save({
        ...dto,
        post: {
          	id: postId
        },
        author,
    })
}
  • comments.module.ts
@Module({
    imports: [
        TypeOrmModule.forFeature([
          	CommentsModel,
        ]),
        CommonModule,
        AuthModule,
        UsersModule,
    ],
    controllers: [CommentsController],
    providers: [CommentsService],
})
export class CommentsModule {}

포스트맨으로 테스트를 해보겠습니다. 로그인 후 토큰 값을 넣은 뒤에 요청합니다.

{
    "comment": "강의 너무 좋아요!!!",
    "post": {
        "id": 101
    },
    "author": {
        "id": 4,
        "updatedAt": "2024-02-18T02:33:34.030Z",
        "createdAt": "2024-02-18T02:33:34.030Z",
        "nickname": "codefactory123",
        "email": "codefactory123@codefactory.ai",
        "role": "USER"
    },
    "id": 1,
    "updatedAt": "2024-02-21T17:43:27.716Z",
    "createdAt": "2024-02-21T17:43:27.716Z",
    "likeCount": 0
}

101에 대한 댓글 페이지네이션을 요청하겠습니다.

{
    "data": [
        {
            "id": 1,
            "updatedAt": "2024-02-21T17:43:27.716Z",
            "createdAt": "2024-02-21T17:43:27.716Z",
            "comment": "강의 너무 좋아요!!!",
            "likeCount": 0
        }
    ],
    "cursor": {
        "after": null
    },
    "count": 1,
    "next": null
}

누가 작성했는지까지 포함하겠습니다.

  • comments.service.ts
paginateComments(
    dto: PaginateCommentsDto,
    postId: number,
) {
    return this.commonService.paginate(
      dto,
      this.commentsRepository,
      {
          where: {
              post: {
                	id: postId,
              }
          },
          relations: { // 추가
            	author: true
          }
      },
      `posts/${postId}/comments`,
    );
}

{
    "data": [
        {
            "id": 1,
            "updatedAt": "2024-02-21T17:43:27.716Z",
            "createdAt": "2024-02-21T17:43:27.716Z",
            "comment": "강의 너무 좋아요!!!",
            "likeCount": 0,
            "author": {
                "id": 4,
                "updatedAt": "2024-02-18T02:33:34.030Z",
                "createdAt": "2024-02-18T02:33:34.030Z",
                "nickname": "codefactory123",
                "email": "codefactory123@codefactory.ai",
                "role": "USER"
            }
        }
    ],
    "cursor": {
        "after": null
    },
    "count": 1,
    "next": null
}

이번에는 특정 Post의 comment정보를 가져오는 요청을 해보겠습니다.

{
    "id": 1,
    "updatedAt": "2024-02-21T17:43:27.716Z",
    "createdAt": "2024-02-21T17:43:27.716Z",
    "comment": "강의 너무 좋아요!!!",
    "likeCount": 0
}

해당 요청에도 override 기능을 추가해보겠습니다.

  • comments.service.ts
async getCommentById(id: number) {
    const comment = await this.commentsRepository.findOne({
        where: {
          	id,
        },
        relations: { // 추가
          	author: true
        }
    });
    if (!comment) throw new BadRequestException(`id: ${id} Comment는 존재하지 않습니다. `);
    return comment;
}

그리고 override 부분에서 계속해서 author: true가 반복됩니다. 따라서 묶어주도록 하겠습니다. FindManyOptions 기능을 이용하겠습니다.

  • comments/const/default-comments-find-options.const.ts
import { FindManyOptions } from "typeorm";
import { CommentsModel } from "../entity/comments.entity";

export const DEFAULT_COMMENT_FIND_OPTIONS: FindManyOptions<CommentsModel> = {
    relations: {
      	author: true
    },
}

서비스 코드를 바꿔줍니다.

  • comments.service.ts
paginateComments(
    dto: PaginateCommentsDto,
    postId: number,
) {
    return this.commonService.paginate(
        dto,
        this.commentsRepository,
        {
            ...DEFAULT_COMMENT_FIND_OPTIONS,
            where: {
                post: {
                  	id: postId,
                }
            },
        },
        `posts/${postId}/comments`,
    );
}

async getCommentById(id: number) {
    const comment = await this.commentsRepository.findOne({
        ...DEFAULT_COMMENT_FIND_OPTIONS,
        where: {
          	id,
        }
    });
    if (!comment) throw new BadRequestException(`id: ${id} Comment는 존재하지 않습니다. `);
    return comment;
}

{
    "data": [
        {
            "id": 1,
            "updatedAt": "2024-02-21T17:43:27.716Z",
            "createdAt": "2024-02-21T17:43:27.716Z",
            "comment": "강의 너무 좋아요!!!",
            "likeCount": 0,
            "author": {
                "id": 4,
                "updatedAt": "2024-02-18T02:33:34.030Z",
                "createdAt": "2024-02-18T02:33:34.030Z",
                "nickname": "codefactory123",
                "email": "codefactory123@codefactory.ai",
                "role": "USER"
            }
        }
    ],
    "cursor": {
        "after": null
    },
    "count": 1,
    "next": null
}

응답 데이터에서 보면 author정보는 사실상 id와 nickname만 필요합니다. 따라서 default option을 바꿔보도록 하겠습니다.

  • default-comments-find-options.const.ts
import { FindManyOptions } from "typeorm";
import { CommentsModel } from "../entity/comments.entity";

export const DEFAULT_COMMENT_FIND_OPTIONS: FindManyOptions<CommentsModel> = {
    relations: {
      	author: true
    },
    select: {
        author: {
            id: true,
            nickname: true,
        }
    }
}

같은 요청을 보내보겠습니다.

{
    "data": [
        {
            "id": 1,
            "updatedAt": "2024-02-21T17:43:27.716Z",
            "createdAt": "2024-02-21T17:43:27.716Z",
            "comment": "강의 너무 좋아요!!!",
            "likeCount": 0,
            "author": {
                "id": 4,
                "nickname": "codefactory123"
            }
        }
    ],
    "cursor": {
        "after": null
    },
    "count": 1,
    "next": null
}

🖊️PATCH: Comment API

  • dto/update-comments-dto.ts
import { PartialType } from "@nestjs/mapped-types";
import { CreateCommentsDto } from "./create-comments.dto";

// CreateCommentsDto의 부분 상속
export class UpdateCommentsDto extends PartialType(CreateCommentsDto) {}
  • comments.controller.ts
@Patch(':commentId')
@UseGuards(AccessTokenGuard)
async patchComment(
    @Param('commentId', ParseIntPipe) commentId: number,
    @Body() body: UpdateCommentsDto,
) {
    return this.commentsService.updateComment(
        body,
        commentId
    )
}
  • comments.service.ts
async updateComment(
    dto: UpdateCommentsDto,
    commentId: number,
) {
  	const comment = await this.commentsRepository.findOne({
        where: {
          	id,
        }
    });
    if (!comment) throw new BadRequestException(`존재하지 않는 댓글입니다. `);
  
    // preload 기능
    const prevComment = await this.commentsRepository.preload({
        id: commentId, // id 기반의 commentId 들어오게 됨
        ...dto, // 나머지는 dto내용으로 변경
    });

    const newComment = await this.commentsRepository.save(
      	prevComment,
    );

    return newComment;
}

포스트맨으로 테스트를 하겠습니다.

{
    "id": 1,
    "updatedAt": "2024-02-21T18:17:05.264Z",
    "createdAt": "2024-02-21T17:43:27.716Z",
    "comment": "NestJS 너무",
    "likeCount": 0
}

GET 요청으로 바꿔졌는지 확인을 해보겠습니다.

{
    "data": [
        {
            "id": 1,
            "updatedAt": "2024-02-21T18:17:05.264Z",
            "createdAt": "2024-02-21T17:43:27.716Z",
            "comment": "NestJS 너무",
            "likeCount": 0,
            "author": {
                "id": 4,
                "nickname": "codefactory123"
            }
        }
    ],
    "cursor": {
        "after": null
    },
    "count": 1,
    "next": null
}

🖊️DELETE: Comment API

  • comments.controller.ts
@Delete(':commentId')
@UseGuards(AccessTokenGuard)
async deleteComment(
  	@Param('commentId', ParseIntPipe) commentId: number,
) {
  	return this.commentsService.deleteComment(commentId);
}
  • comments.service.ts
async deleteComment(
  	id: number
) {
    const comment = await this.commentsRepository.findOne({
        where: {
          	id,
        }
    });
    if (!comment) throw new BadRequestException(`존재하지 않는 댓글입니다. `);

    await this.commentsRepository.delete(id);
    return id;
}

포스트맨으로 테스트를 해보겠습니다.

{
    "raw": [],
    "affected": 1
}

GET 요청으로 확인하면 4번이 사라진 것을 알 수 있습니다.


🖊️Path Parameter 검증 Middleware

현재 endpoint 경로는 post가 존재하면 에러를 던지는 코드는 존재하지 않습니다. 이 부분은 Middleware로 적용을 해보겠습니다. Middleware는 가장 앞단에서 먼저 필터링을 시작합니다.

컨트롤러에서 전반적으로 post가 존재하지 않으면 전부 BadRequestException을 던지도록 하겠습니다. 먼저 middleware 코드를 작성하겠습니다.

  • comments/middleware/post-exists.middleware.ts
import { BadRequestException, Injectable, NestMiddleware } from "@nestjs/common";
import { NextFunction, Request, Response } from "express";
import { PostsService } from "src/posts/posts.service";

@Injectable()
export class PostExistsMiddleware implements NestMiddleware {

    constructor(
    	private readonly postService: PostsService, 
    ) {}

    async use(req: Request, res: Response, next: NextFunction) {
        const postId = req.params.postId; // path parameter 안의 postId를 가져올 수 있습니다.
        if (!postId) throw new BadRequestException(`Post ID 파라미터는 필수입니다. `);
        const exists = await this.postService.checkPostExistsById(
          	parseInt(postId),
        );
        if (!exists) throw new BadRequestException(`Post가 존재하지 않습니다. `);
        next(); // next를 해줘야 다음단계로 이동
    }
}
  • posts.service.ts
async checkPostExistsById(id: number) {
    return this.postsRepository.exists({
        where: {
          	id,
        },
    })
}

이제 PostExists Middleware를 등록하겠습니다. Middleware를 등록하려면 등록할 module로 가서 implements를 합니다.

  • comments.module.ts
@Module({
    imports: [
        TypeOrmModule.forFeature([
          	CommentsModel,
        ]),
        CommonModule,
        AuthModule,
        UsersModule,
        PostsModule, // 등록
    ],
    controllers: [CommentsController],
    providers: [CommentsService],
})
export class CommentsModule implements NestModule {

    configure(consumer: MiddlewareConsumer) {
        consumer
            .apply(PostExistsMiddleware) // 적용할 Middleware
            .forRoutes(CommentsController); // 적용할 Controller 전체
    }
}

포스트맨으로 테스트를 하겠습니다. 존재하지 않는 Post를 조회하겠습니다. 만약 comment와 관련된 endpoint로 199번을 조회하면 전부 동일한 에러 메세지를 보내줄 것입니다.

{
    "message": "Post가 존재하지 않습니다. ",
    "error": "Bad Request",
    "statusCode": 400
}

이런식으로 Middleware를 적용하는 것이 좋은 사례인 것을 알 수 있습니다.

profile
현재 블로그 : https://jasonsong97.tistory.com/

14개의 댓글

comment-user-thumbnail
2024년 4월 24일

I appreciate module nesting. This technique allows me to organize my application's functionality into hierarchical modules, promoting Wordle Unlimited code reusability, maintainability, and scalability.

답글 달기
comment-user-thumbnail
2024년 4월 24일

Really interesting programs to learn more. Explore the space and enjoy a deeper understanding of the rules. Continuously update advanced knowledge basketbros

답글 달기
comment-user-thumbnail
2024년 7월 22일

I really value module nesting for its ability to structure my application's features into layered modules. It’s a great way to boost code level devil reusability, simplify maintenance, and scale up effectively.

답글 달기
comment-user-thumbnail
2024년 8월 14일

In NestJS, Skribbl IO module nesting is a powerful feature that helps you organize your application in a modular and maintainable way.

답글 달기
comment-user-thumbnail
2024년 10월 20일

It's useful. Module nesting refers to the process of nesting other components or modules within a page component or layout component. This nesting can help you build complex page structures and improve code maintainability and reusability.

답글 달기
comment-user-thumbnail
2024년 12월 20일

For anyone looking to incorporate engaging features, I recommend exploring the block blast game as a fun project alongside this API. This can help you sharpen your skills in handling states, improving user interactions, and learning about game mechanics. Combining backend development with a game project can provide a holistic learning experience.
https://block-blast.online/

답글 달기
comment-user-thumbnail
2025년 2월 26일

I have learned a lot from this, thank you! Learning NestJS makes me curious and fascinated, just like when I use a Morse code translator.

답글 달기
comment-user-thumbnail
2025년 4월 23일

One of the greatest and most dependable Roblox exploits, created by Ice Bear, is Krnl executor https://krnl.vip/ https://thekrnl.com/

답글 달기
comment-user-thumbnail
2025년 4월 23일

Because it enables them to win with little effort, gamers love this application. To win these quick Free Fire matches, download the FFH4X Injector. https://ffh4x.vip/ https://fisch-macro.com/

답글 달기
comment-user-thumbnail
2025년 6월 5일

chicken jockey clicker features a blocky environment and pixelated textures that perfectly capture the iconic Minecraft aesthetic. You’ll find yourself in a quirk

답글 달기
comment-user-thumbnail
2025년 6월 5일

You may play your favorite games for free online. No installs or downloads. With just one click on now.gg, you can play games continuously on any device. bitlife

답글 달기
comment-user-thumbnail
2025년 6월 5일

You may play your favorite games for free online. No installs or downloads. With just one click on now.gg, you can play games continuously on any device. bitlife

답글 달기
comment-user-thumbnail
2025년 6월 5일

You may play your favorite games for free online. No installs or downloads. With just one click on now.gg, you can play games continuously on any device. bitlife

답글 달기
comment-user-thumbnail
2025년 6월 5일

You may play your favorite games for free online. No installs or downloads. With just one click on now.gg, you can play games continuously on any device. bitlife

답글 달기