nest g mo board
nest g co board
nest g s board
// lodash 설치, 루트폴더에서
npm i lodash
"esModuleInterop": true
추가import { Controller, Delete, Get, Post, Put } from '@nestjs/common';
import { BoardService } from './board.service';
@Controller('board') // routing path is /board
export class BoardController {
// 서비스 주입
constructor(private readonly boardService: BoardService) {}
// 게시물 목록을 가져오는 api
@Get('articles')
getArticles() {
return this.boardService.getArticles();
}
// 게시물 상세보기 -> 게시물 ID로
@Get('/articles/:id')
getArticleById() {
return this.boardService.getArticleById(id);
}
// 게시글 작성
@Post('/articles')
createArticle() {
return this.boardService.createArticle();
}
// 게시물 수정
@Put('/articles/:id')
updateArticle() {
return this.boardService.updateArticle(id);
}
// 게시물 삭제
@Delete('/articles/:id')
deleteArticle() {
return this.boardService.deleteArticle(id);
}
}
POST, PUT의 경우에는 클라이언트로부터 데이터를 전달받아야 게시물 생성을 하건, 수정을 하건 할 텐데 말이죠. Express에서는 req.body에 담긴 데이터를 인출해서 썼었는데 여기서는 어떻게 해야 할까요?
Nest.js에서는 클라이언트로부터 데이터를 받거나 데이터를 줘야 할 때는 DTO를 사용해야 합니다.
DTO에 대해서 간단하게 정의하자면 데이터를 전송하기 위해 작성된 객체라고 생각하시면 됩니다. Nest.js에서는 모든 데이터는 DTO를 통해서 운반되니 꼭 잊지 말아 주세요!
직접 DTO 객체를 작성해보기 전에, 우리는 class-validator, class-transformer라는 패키지를 설치를 할 거에요!
class-validator는 입력값 유효성 검사를 위해 정말 다양한 기능을 제공합니다.
npm i class-validator class-transformer
create-article.dto.ts
import { IsNumber, IsString } from 'class-validator';
export class CreateArticleDto {
@IsString()
readonly title: string;
@IsString()
readonly content: string;
@IsNumber()
readonly password: number;
}
update-article.dto.ts 1차
import { IsNumber, IsString } from 'class-validator';
export class UpdateArticleDto {
@IsString()
readonly title: string;
@IsString()
readonly content: string;
@IsNumber()
readonly password: number;
}
delete-article.dto.ts 1차
import { IsNumber } from 'class-validator';
export class DeleteArticleDto {
@IsNumber()
readonly password: number;
}
DTO를 3개 작성한 이유는 각각의 요청마다 해당 요청으로부터 전달을 받는 데이터가 조금씩 다 다르기 때문에 통상적으로 요청 1 : 1 DTO
비율을 유지합니다. 그런데, update-article.dto.ts는 create-article.dto.ts에서 사실상 다를게 없죠? 이럴 때는 코드 복사 + 붙여넣기도 좋지만 @nestjs/mapped-types
의 PartialType
을 상속받으면 이를 깔끔하게 해결할 수 있습니다.
조금만 더 설명을 해보면 게시물을 수정할 때도 title, content, password를 동일하게 받아야 합니다. 이것을 PartialType을 상속받아서 해결하면 UpdateArticleDto는 CreateArticleDto 클래스의 부분집합이다.
라고 선언을 하게 되는 것이고 코드 복사 + 붙여넣기를 하지 않아도 동일한 효력을 가질 수 있습니다! 부분집합은 해당 필드가 전부 포함되어도 성립되고 특정 필드들이 생략이 되어도 성립이 되니까 문제가 없습니다!
그렇다면, DeleteArticleDto도 동일하게 할 수 있을까요? 네. 당연합니다! 하지만, 게시물을 삭제하는데 title, content를 굳이 선택적으로 받게끔 여지를 줄 필요는 없겠죠? 이럴 때는 더욱 더 단호하게 @nestjs/mapped-types
의 PickType
을 상속받아 나는 이 부모 클래스에서 이 필드만 필요해!를 선언하는 것이 더욱 더 깔끔합니다.
npm i @nestjs/mapped-types
update-article.dto.ts 2차
import { PartialType } from '@nestjs/mapped-types';
import { CreateArticleDto } from './create-article.dto';
export class UpdateArticleDto extends PartialType(CreateArticleDto) {}
delete-article.dto.ts 2차
import { PickType } from '@nestjs/mapped-types';
import { CreateArticleDto } from './create-article.dto';
export class DeleteArticleDto extends PickType(CreateArticleDto, [
'password',
] as const) {}
아예 새로운 유형의 DTO를 작성하는 것이 아니면 항상
@nestjs/mapped-types
사용을 습관화하여 생산성을 높여주세요!또한, DTO의 유효성 검사를 하기 위해서는 main.ts에 ValidationPipe를 주입해야 해요.
main.ts
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();
DTO를 작성하면 필요한 곳에서는 어디서든지 자유롭게 쓸 수 있고 유효성 검사 데코레이터는 DTO에서 한 번만 정의되면 됨!!
dto를 사용해서 코드 수정!
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
} from '@nestjs/common';
import { BoardService } from './board.service';
import { CreateArticleDto } from './create-article.dto';
import { DeleteArticleDto } from './delete-article.dto';
import { UpdateArticleDto } from './update-article.dto';
@Controller('board') // routing path is /board
export class BoardController {
// 서비스 주입
constructor(private readonly boardService: BoardService) {}
// 게시물 목록을 가져오는 api
@Get('articles')
getArticles() {
return this.boardService.getArticles();
}
// 게시물 상세보기 -> 게시물 ID로
@Get('/articles/:id')
getArticleById(@Param('id') articleId: number) {
return this.boardService.getArticleById(articleId);
}
// 게시글 작성
@Post('/articles')
createArticle(@Body() data: CreateArticleDto) {
return this.boardService.createArticle(
data.title,
data.content,
data.password,
);
}
// 게시물 수정
@Put('/articles/:id')
updateArticle(
@Param('id') articleId: number,
@Body() data: UpdateArticleDto,
) {
return this.boardService.updateArticle(
articleId,
data.title,
data.content,
data.password,
);
}
// 게시물 삭제
@Delete('/articles/:id')
deleteArticle(
@Param('id') articleId: number,
@Body() data: DeleteArticleDto,
) {
return this.boardService.deleteArticle(articleId, data.password);
}
}
import {
Body,
Controller,
Delete,
Get,
Param,
Post,
Put,
} from '@nestjs/common';
import { BoardService } from './board.service';
import { CreateArticleDto } from './create-article.dto';
import { DeleteArticleDto } from './delete-article.dto';
import { UpdateArticleDto } from './update-article.dto';
@Controller('board') // routing path is /board
export class BoardController {
// 서비스 주입
constructor(private readonly boardService: BoardService) {}
// 게시물 목록을 가져오는 api
@Get('articles')
getArticles() {
return this.boardService.getArticles();
}
// 게시물 상세보기 -> 게시물 ID로
@Get('/articles/:id')
getArticleById(@Param('id') articleId: number) {
return this.boardService.getArticleById(articleId);
}
// 게시글 작성
@Post('/articles')
createArticle(@Body() data: CreateArticleDto) {
return this.boardService.createArticle(
data.title,
data.content,
data.password,
);
}
// 게시물 수정
@Put('/articles/:id')
updateArticle(
@Param('id') articleId: number,
@Body() data: UpdateArticleDto,
) {
return this.boardService.updateArticle(
articleId,
data.title,
data.content,
data.password,
);
}
// 게시물 삭제
@Delete('/articles/:id')
deleteArticle(
@Param('id') articleId: number,
@Body() data: DeleteArticleDto,
) {
return this.boardService.deleteArticle(articleId, data.password);
}
}
못보던 데코레이터인 @Param과 @Body가 생겼습니다. 여러분들이 생각하시는 그 파라미터와 바디가 맞습니다.
/articles/:id
에서 :id는 파라미터를 뜻합니다. 그래서 저 URI에 지정된 파라미터를 가져오기 위해서는 @Param
이라는 데코레이터에 갖고 올 파라미터 이름을 넘겨야합니다. 여기서는 id라는 파라미터를 갖고 오는 것이기 때문에 @Param(’id’)
라고 갖고 온 후 number 타입의 articleId로 id라는 파라미터를 받겠다는 얘기죠!
바디를 갖고 오는 것은 @Body
데코레이터를 사용하면 됩니다. req.body로 전달되는 데이터를 UpdateArticleDto라는 타입의 객체의 data라는 변수로 받겠다는 의미입니다! 당연히, 잘못된 데이터가 있으면 ValidationPipe로 인해서 400 리턴이 자동으로 될 것입니다
import {
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import _ from 'lodash';
@Injectable()
export class BoardService {
// 데이터베이스를 사용하지 않아 일단은 배열로 구현을 하였으니 오해 말아주세요!
// 보통은 TypeORM 모듈을 이용하여 리포지토리를 의존합니다. 이건 나중에 배울게요!
private articles = [];
// 게시글 비밀번호를 저장하기 위한 Map 객체입니다.
private articlePasswords = new Map();
getArticles() {
return this.articles;
}
getArticleById(id: number) {
return this.articles.find((article) => return article.id === id);
}
createArticle(title: string, content: string, password: number) {
const articleId = this.articles.length + 1;
this.articles.push({ id: articleId, title, content });
this.articlePasswords.set(articleId, password);
return articleId;
}
updateArticle(id: number, title: string, content: string, password: number) {
if (this.articlePasswords.get(id) !== password) {
throw new UnauthorizedException(
`Article password is not correct. id: ${id}`,
);
}
const article = this.getArticleById(id);
if (_.isNil(article)) {
throw new NotFoundException(`Article not found. id: ${id}`);
}
article.title = title;
article.content = content;
}
deleteArticle(id: number, password: number) {
if (this.articlePasswords.get(id) !== password) {
throw new UnauthorizedException(
`Article password is not correct. id: ${id}`,
);
}
this.articles = this.articles.filter((article) => article.id !== id);
}
}