이전 글
- [nestjs] restapi + prisma 01
- [nestjs] restapi + prisma 02
- [nestjs] restapi + prisma 03
- [nestjs] restapi + prisma 04
PART4
NestJS REST API에서 관계형 데이터를 처리하는 방법을 배웁니다.
이 장에서는 데이터 계층과 API 계층에서 관계형 데이터를 처리하는 방법을 배웁니다.
먼저, 일대다 관계 기사 레코드(즉, 한 사용자가 여러 기사를 가질 수 있음)가 있는 데이터베이스 스키마에 사용자 모델을 추가합니다.
다음으로 사용자 레코드에 대한 CRUD(만들기, 읽기, 업데이트 및 삭제) 작업을 수행하기 위해 사용자 끝점에 대한 API 경로를 구현합니다.
마지막으로 API 계층에서 사용자-기사 관계를 모델링하는 방법을 배웁니다.
DB에 User 모델 추가
현재는 Article 모델만 있는데, 이에 User 모델을 추가합니다.
이후 npx prisma migrate dev --name "add-user-model"
명령어를 실행합니다.
// prisma/schema.prisma
model Article {
id Int @id @default(autoincrement())
title String @unique
description String?
body String
published Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
articles Article[]
}
seed 스크립트 수정
최초 더미 데이터 적재를 위하여 seed.ts
스크립트를 수정합니다.
이후 npx prisma db seed
명령어를 실행합니다.
async function main() {
// create two dummy users
const user1 = await prisma.user.upsert({
where: { email: '[email protected]' },
update: {},
create: {
email: '[email protected]',
name: 'Sabin Adams',
password: 'password-sabin',
},
});
const user2 = await prisma.user.upsert({
where: { email: '[email protected]' },
update: {},
create: {
email: '[email protected]',
name: 'Alex Ruheni',
password: 'password-alex',
},
});
// create three dummy articles
const post1 = await prisma.article.upsert({
where: { title: 'Prisma Adds Support for MongoDB' },
update: {
authorId: user1.id,
},
create: {
title: 'Prisma Adds Support for MongoDB',
body: 'Support for MongoDB has been one of the most requested features since the initial release of...',
description:
"We are excited to share that today's Prisma ORM release adds stable support for MongoDB!",
published: false,
authorId: user1.id,
},
});
const post2 = await prisma.article.upsert({
where: { title: "What's new in Prisma? (Q1/22)" },
update: {
authorId: user2.id,
},
create: {
title: "What's new in Prisma? (Q1/22)",
body: 'Our engineers have been working hard, issuing new releases with many improvements...',
description:
'Learn about everything in the Prisma ecosystem and community from January to March 2022.',
published: true,
authorId: user2.id,
},
});
const post3 = await prisma.article.upsert({
where: { title: 'Prisma Client Just Became a Lot More Flexible' },
update: {},
create: {
title: 'Prisma Client Just Became a Lot More Flexible',
body: 'Prisma Client extensions provide a powerful new way to add functionality to Prisma in a type-safe manner...',
description:
'This article will explore various ways you can use Prisma Client extensions to add custom functionality to Prisma Client..',
published: true,
},
});
console.log({ user1, user2, post1, post2, post3 });
}
Article Entity 수정
// src/articles/entities/article.entity.ts
import { Article } from '@prisma/client';
import { ApiProperty } from '@nestjs/swagger';
export class ArticleEntity implements Article {
@ApiProperty()
id: number;
@ApiProperty()
title: string;
@ApiProperty({ required: false, nullable: true })
description: string | null;
@ApiProperty()
body: string;
@ApiProperty()
published: boolean;
@ApiProperty()
createdAt: Date;
@ApiProperty()
updatedAt: Date;
@ApiProperty({ required: false, nullable: true })
authorId: number | null;
}
user 를 위한 resource 생성하기
npx nest generate resource
- users
- REST API
- Yes
위와 같이 선택하여 생성합니다. src/users 폴더가 생성되었을 것입니다.
http://localhost:3000/api 에 접속하면 swagger default 에 user 관련 API가 추가된 것을 확인할 수 있습니다.
PrismaClient 에 Users 모듈 추가하기
- user module 에 prisma module 을 추가
- user service 에 prisma service 를 추가
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { PrismaModule } from 'src/prisma/prisma.module';
@Module({
controllers: [UsersController],
providers: [UsersService],
imports: [PrismaModule],
})
export class UsersModule {}
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { PrismaService } from 'src/prisma/prisma.service';
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
// CRUD operations
}
사용자 entity 설정
DTO 란 (data transfer object 데이터 전송 개체) 데이터가 네트워크를 통해 전송되는 방법을 정의하는 개체임
password 의 경우 swagger 에서 노출되지 않도록 설정합니다. (@ApiProperty()
를 사용하지 않으면 됨)
사용자를 생성하고 업데이트할 때 각각 API로 보낼 데이터를 정의하려면 CreateUserDto 및 UpdateUserDto 클래스를 구현해야 합니다.
다음과 같이 create-user.dto.ts 파일 내에서 CreateUserDto 클래스를 정의합니다.
UpdateUserDto의 정의는 CreateUserDto 정의에서 자동으로 유추되므로 명시적으로 정의할 필요가 없습니다.
// src/users/entities/user.entity.ts
import { ApiProperty } from '@nestjs/swagger';
import { User } from '@prisma/client';
export class UserEntity implements User {
@ApiProperty()
id: number;
@ApiProperty()
createdAt: Date;
@ApiProperty()
updatedAt: Date;
@ApiProperty()
name: string;
@ApiProperty()
email: string;
password: string;
}
// src/users/dto/create-user.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
export class CreateUserDto {
@IsString()
@IsNotEmpty()
@ApiProperty()
name: string;
@IsString()
@IsNotEmpty()
@ApiProperty()
email: string;
@IsString()
@IsNotEmpty()
@MinLength(6)
@ApiProperty()
password: string;
}
User 서비스 정의
UsersService는 Prisma 클라이언트를 사용하여 데이터베이스에서 데이터를 수정 및 가져오고 이를 UsersController에 제공하는 일을 담당합니다.
이 클래스에서 create(), findAll(), findOne(), update() 및 remove() 메서드를 구현합니다.
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { PrismaService } from 'src/prisma/prisma.service';
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
create(createUserDto: CreateUserDto) {
return this.prisma.user.create({ data: createUserDto });
}
findAll() {
return this.prisma.user.findMany();
}
findOne(id: number) {
return this.prisma.user.findUnique({ where: { id } });
}
update(id: number, updateUserDto: UpdateUserDto) {
return this.prisma.user.update({ where: { id }, data: updateUserDto });
}
remove(id: number) {
return this.prisma.user.delete({ where: { id } });
}
}
User 컨트롤러 정의
// src/users/users.controller.ts
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
ParseIntPipe,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { UserEntity } from './entities/user.entity';
@Controller('users')
@ApiTags('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
@ApiCreatedResponse({ type: UserEntity })
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Get()
@ApiOkResponse({ type: UserEntity, isArray: true })
findAll() {
return this.usersService.findAll();
}
@Get(':id')
@ApiOkResponse({ type: UserEntity })
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id);
}
@Patch(':id')
@ApiCreatedResponse({ type: UserEntity })
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
) {
return this.usersService.update(id, updateUserDto);
}
@Delete(':id')
@ApiOkResponse({ type: UserEntity })
remove(@Param('id', ParseIntPipe) id: number) {
return this.usersService.remove(id);
}
}
User 응답에서 password 제거
예상대로 동작하지만 응답에 암호가 포함되는 중대한 보안 결함이 있습니다.
이를 해결하기 위한 2가지 방법이 존재 합니다.
- 컨트롤러 경로 처리기의 응답 본문에서 암호를 수동으로 제거
- 인터셉터를 사용하여 응답 본문에서 암호를 자동으로 제거
첫 번째 옵션은 오류가 발생하기 쉽고 불필요한 코드 중복을 초래합니다. 따라서 두 번째 방법을 사용합니다.
Interceptor 를 사용하여 응답 필드의 값을 제거하여 반환
NestJS의 인터셉터를 사용하면 요청-응답 주기에 연결할 수 있고 경로 처리기가 실행되기 전과 후에 추가 논리를 실행할 수 있습니다.
이 경우 응답 본문에서 암호 필드를 제거하는 데 사용합니다.
NestJS에는 개체를 변환하는 데 사용할 수 있는 내장 ClassSerializerInterceptor가 있습니다. 이 인터셉터를 사용하여 응답 개체에서 암호 필드를 제거합니다.
먼저 main.ts를 업데이트하여 ClassSerializerInterceptor를 전역적으로 활성화합니다.
생성자는 개체를 사용하고 Object.assign() 메서드를 사용하여 부분 개체에서 UserEntity 인스턴스로 속성을 복사합니다.
부분 유형은 Partial<UserEntity>
입니다. 이는 부분 객체가 UserEntity 클래스에 정의된 속성의 하위 집합을 포함할 수 있음을 의미합니다.
다음으로 Prisma.User
개체 대신 UserEntity
를 반환하도록 UsersController 경로 처리기를 업데이트합니다.
// src/main.ts
import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));
const config = new DocumentBuilder()
.setTitle('Median')
.setDescription('The Median API description')
.setVersion('0.1')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();
// src/users/entities/user.entity.ts
import { ApiProperty } from '@nestjs/swagger';
import { User } from '@prisma/client';
import { Exclude } from 'class-transformer';
export class UserEntity implements User {
@ApiProperty()
id: number;
@ApiProperty()
createdAt: Date;
@ApiProperty()
updatedAt: Date;
@ApiProperty()
name: string;
@ApiProperty()
email: string;
@Exclude()
password: string;
}
Article 에 User 상세정보 포함
Article 개체와 함께 작성자를 반환하여 이를 개선할 수 있습니다.
데이터 액세스 논리는 ArticlesService 내부에서 구현됩니다.
Article 개체와 함께 작성자를 반환하도록 findOne() 메서드를 업데이트합니다.
// src/articles/articles.service.ts
findOne(id: number) {
return this.prisma.article.findUnique({
where: { id },
include: {
author: true,
},
});
}
하지만, 또 author 정보에 password 가 노출기 때문에 Article 정보를 update 하도록 한다
UserEntity 추가 및 생성자에서 UserEntity 초기화를 진행
// src/articles/entities/article.entity.ts
import { Article } from '@prisma/client';
import { ApiProperty } from '@nestjs/swagger';
import { UserEntity } from 'src/users/entities/user.entity';
export class ArticleEntity implements Article {
@ApiProperty()
id: number;
@ApiProperty()
title: string;
@ApiProperty({ required: false, nullable: true })
description: string | null;
@ApiProperty()
body: string;
@ApiProperty()
published: boolean;
@ApiProperty()
createdAt: Date;
@ApiProperty()
updatedAt: Date;
@ApiProperty({ required: false, nullable: true })
authorId: number | null;
@ApiProperty({ required: false, type: UserEntity })
author?: UserEntity;
constructor({ author, ...data }: Partial<ArticleEntity>) {
Object.assign(this, data);
if (author) {
this.author = new UserEntity(author);
}
}
}
이제 ArticleEntity 개체의 인스턴스를 반환하도록 ArticlesController를 업데이트합니다.
return 될 결과를 new ArticleEntity
로 wrap 해주면 됨
// src/articles/articles.controller.ts
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
ParseIntPipe,
} from '@nestjs/common';
import { ArticlesService } from './articles.service';
import { CreateArticleDto } from './dto/create-article.dto';
import { UpdateArticleDto } from './dto/update-article.dto';
import { ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { ArticleEntity } from './entities/article.entity';
@Controller('articles')
@ApiTags('articles')
export class ArticlesController {
constructor(private readonly articlesService: ArticlesService) {}
@Post()
@ApiCreatedResponse({ type: ArticleEntity })
async create(@Body() createArticleDto: CreateArticleDto) {
return new ArticleEntity(
await this.articlesService.create(createArticleDto),
);
}
@Get()
@ApiOkResponse({ type: ArticleEntity, isArray: true })
async findAll() {
const articles = await this.articlesService.findAll();
return articles.map((article) => new ArticleEntity(article));
}
@Get('drafts')
@ApiOkResponse({ type: ArticleEntity, isArray: true })
async findDrafts() {
const drafts = await this.articlesService.findDrafts();
return drafts.map((draft) => new ArticleEntity(draft));
}
@Get(':id')
@ApiOkResponse({ type: ArticleEntity })
async findOne(@Param('id', ParseIntPipe) id: number) {
return new ArticleEntity(await this.articlesService.findOne(id));
}
@Patch(':id')
@ApiCreatedResponse({ type: ArticleEntity })
async update(
@Param('id', ParseIntPipe) id: number,
@Body() updateArticleDto: UpdateArticleDto,
) {
return new ArticleEntity(
await this.articlesService.update(id, updateArticleDto),
);
}
@Delete(':id')
@ApiOkResponse({ type: ArticleEntity })
async remove(@Param('id', ParseIntPipe) id: number) {
return new ArticleEntity(await this.articlesService.remove(id));
}
}
PART4 끝
이 장에서는 Prisma를 사용하여 NestJS 애플리케이션에서 관계형 데이터를 모델링하는 방법을 배웠습니다.
또한 ClassSerializerInterceptor와 엔터티 클래스를 사용하여 클라이언트에 반환되는 데이터를 제어하는 방법에 대해서도 배웠습니다.
[광고] STEEM 개발자 커뮤니티에 참여 하시면, 다양한 혜택을 받을 수 있습니다.
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit
Upvoted! Thank you for supporting witness @jswit.
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit