[NestJS] - 6. 상품 REST API 만들기
이번 포스팅에서는 중고마켓에 사용될 상품 데이터의 CRUD를 하는 REST API를 NestJS로 만들어보려고 한다.
프로젝트 구조
- products 디렉터리 안에 모듈 구조를 만들어둔다 - products 디렉터리 안에 dto와 interfaces 디렉터리를 만든다. - dto는 요청 Input으로 들어오는 데이터 타입을 정의한다. - interface는 요청 결과 등으로 쓰이는 데이터의 인터페이스를 정의한다. - app.module.ts에서는 ProductModule을 imports에 추가한다. |
모듈
▶ products.module.ts
import { Module } from '@nestjs/common';
import { ProductsController } from './products.controller';
import { ProductsService } from './products.service';
@Module({
controllers: [ProductsController],
providers: [ProductsService],
})
export class ProductsModule {}
사용할 컨트롤러와 서비스를 의존성 주입을 위해 모듈에 선언한다.
컨트롤러
컨트롤러단에서 API Request Mapping을 추가하여 다음의 REST API을 만들려고 한다. 업데이트는 만들지 않았으나 적용하는 방식은 비슷하다.
메서드 | 엔드포인트 | 기능 |
GET | /products | 상품 전체 조회 |
GET | /products/:id | ID에 해당하는 상품 한개 조회 |
POST | /products | 상품 추가 |
DELETE | /products/:id | ID에 해당하는 상품 삭제 |
▶ products.controller.ts
import { Body, Controller, Delete, Get, Param, Post } from '@nestjs/common';
import { CreateProductInput } from './dto/createProduct.dto';
import { Product } from './interfaces/product.interface';
import { ProductsService } from './products.service';
@Controller('products')
export class ProductsController {
constructor(private readonly productService: ProductsService) {}
@Post()
createProduct(@Body() createProductInput: CreateProductInput): boolean {
console.log(createProductInput);
return this.productService.create(createProductInput);
}
@Get(':id')
fetchProduct(@Param('id') id: number): Product {
return this.productService.findOne(id);
}
@Get()
fetchProducts(): Product[] {
return this.productService.findAll();
}
@Delete(':id')
deleteProduct(@Param('id') id: number): boolean {
return this.productService.delete(id);
}
}
@Controller에 'products'라는 경로 접두어를 붙여, 해당 컨트롤러에서 생성하는 엔드포인트들은 항상 앞에 /products/ 경로를 사용하게 된다.
fetchProducts() : 상품 리스트 조회
- @Get() 데코레이터를 통해 요청 메서드(GET)와 엔드포인트('/products' ) 정의
- Request에서 Query Param이나 Path Param 혹은 Body로 아무 인자를 받지 않고 상품 리스트를 반환.
- 타입스크립트로 작성하여 반환형을 Product[]로 명시해주었으며, Product는 interface에서 정의해주었다.
fetchProduct(id) : 단일 상품 조회
- @Get(:id) 데코레이터를 통해 요청 메서드(GET)와 엔드포인트('/products/1234' ) 정의
- Request에서 Path Parameter로 ID값을 받아 해당 ID에 해당하는 상품을 반환한다.
- Path Parameter는 엔드포인트 경로에서 :(변수)의 형태로 지정하고, @Param 데코레이터를 통해 Argument로 사용한다.
- 반환형은 단일 상품이므로 Product
- + 만약 Query Parameter(/products?id=1234)를 사용하고 싶다면 아래와 같이 사용한다. 단 이 때의 엔드포인트는 ('/products') 이므로 fetchProducts에서 쿼리 파라미터를 받아서 작동해야 한다.
fetchProducts(@Query('number') num: number)
deleteProduct(id) : 단일 상품 삭제
- @Delete(:id) 데코레이터를 통해 요청 메서드(DELETE)와 엔드포인트('/products/1234' ) 정의
- Request에서 Path Parameter로 ID값을 받아 해당 ID에 해당하는 상품을 삭제한다.
- 반환형은 성공/실패를 가리기 위해서 boolean으로 설정했다.
createProduct(createProductInput) : 상품 추가
- @Post() 데코레이터를 통해 요청 메서드(POST)와 엔드포인트('/products' ) 정의
- Request에서 Body로 데이터 모델을 받아 상품을 추가한다
- Request Body의 데이터를 @Body 데코레이터와 모델 타입을 정의하여 Argument로 받아 사용할 수 있다.
- 해당 API는 CreateBoardInput형의 Body의 데이터를 받아 사용하겠다는 뜻이다.
- 반환형은 성공/실패를 가리기 위해서 boolean으로 설정했다.
- CreatrBoardInput은 요청으로 들어오는 데이터 타입을 정의해야 하므로 dto로 작성하였다.
DTO와 Interface 정의
NestJS는 타입스크립트 기반의 프레임워크이기에 Input과 Output에 형을 명시해주는 것은 오류를 사전 방지하기에 아주 좋다.
위의 컨트롤러에서도 요청과 응답 객체에서 사용하기 위해서 데이터 모델을 만들어 사용했다.
DTO
DTO는 Data Transfer Object, 계층 간 데이터 교환을 하기 위해 사용하는 객체이다. 즉 클라이언트의 요청의 입력값을 서버로 전송하여 받을 때 사용될 객체이다. 짐들을 배달할 때 마차 안 정리함에 정렬되어 오는 것과, 그냥 무작위로 오는 것의 차이를 생각하면 되겠다.
비슷하게 DAO(Data Access Object)라는 개념도 있는데 NestJS에서는 DTO와 구분지어서 사용하지는 않는 것 같지만 이것은 서버와 데이터베이스와의 데이터 교환을 하기 위해 사용하는 객체이다. |
▶ createProduct.dto.ts
import { IsInt, IsString } from 'class-validator';
export class CreateProductInput {
@IsString()
name: string;
@IsString()
description: string;
@IsInt()
price: number;
}
상품 등록을 할 때 Request Body에서 받을 DTO을 정의한 클래스이다.
여기서 Class-Validator라는 것을 사용했는데, 이는 요청 시 데이터의 유효성 검사를 제공해준다.
위처럼 데코레이터를 통해 사전에 잘못된 데이터 자료형을 막을 수 있다. @IsString()은 String 아니면 막아버린다.
Class-Validator을 사용하려면 패키지를 다운받아야 한다.
yarn add class-validator class-transformer |
▶ 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();
그리고 main.ts의 app에 GlobalPipe로서 등록해준다. Pipe는 요청을 필터링해주는 역할을 한다는 의미로 대충 알아두자.
Interface
▶ product.interface.ts
export interface Product {
id: number;
name: string;
description: string;
price: number;
}
데이터를 Response로 함께 응답해주어야 할 때도 Object 형태이고 활용할 수 있는 형이 없다면, 인터페이스를 활용해서 간단하게 표기해주는 것이 좋다.
상품 리스트 조회나 상품 단일 조회 시 반환형을 명시하기 위해서 인터페이스를 새로 만들어주었다.
서비스
import { Injectable } from '@nestjs/common';
import { CreateProductInput } from './dto/createProduct.dto';
import { Product } from './interfaces/product.interface';
@Injectable()
export class ProductsService {
private incrementID = 1;
private products: Product[] = [];
create(input: CreateProductInput) {
console.log(input);
const product = {
id: this.incrementID,
...input,
};
this.incrementID++;
this.products.push(product);
return true;
}
findAll() {
return this.products;
}
findOne(id: number) {
console.log(id);
const product = this.products.find((el) => {
if (el.id == id) {
return true;
}
});
return product;
}
delete(id: number) {
const idx = this.products.findIndex((el) => {
if (el.id == id) {
return true;
}
});
console.log(idx);
if (idx == -1) {
return false;
}
this.products.splice(idx, 1);
return true;
//
}
}
컨트롤러에서 사용되는 비즈니스 로직들을 만들어주었다. 아직 데이터베이스를 연결하지 않았으므로 야매로 App 내부의 배열을 DB라고 가정하고 로직들을 작성하였다.
실행 결과
API들이 요청을 잘 받아 예상한 응답을 주는 것을 확인해 볼 수 있었다.
다음 포스팅에서는 데이터베이스를 사용하기 위해서 편리하게 쓸 수 있는 ORM을 적용시키는 것을 다루겠다.
Reference
https://github.com/nestjs/nest/tree/master/sample/01-cats-app
해당 강의를 들으면서 학습한 내용을 바탕으로 저만의 프로젝트를 만드는 과정을 기록하여 남기는 것을 목표로 하고 있습니다.
주관적인 생각이 들어가 있을 수 있으므로 혹시 틀린 내용이 있다면 피드백 부탁드립니다.
'Backend > Node.js (NestJS)' 카테고리의 다른 글
[NestJS] - 8. 데이터베이스 관계와 TypeORM 데이터베이스 관계 매핑 (0) | 2023.02.22 |
---|---|
[NestJS] - 7. TypeORM 데이터베이스 연동과 엔티티 매핑 (0) | 2023.02.22 |
[NestJS] - 5. 모듈 의존성 주입, 다른 모듈의 서비스를 사용해보자 (0) | 2023.02.09 |
[NestJS] - 4. DI / IoC (의존성 주입/제어의 역전) (0) | 2023.02.09 |
[NestJS] - 3. 모듈 구조 - Module/Service/Controller (0) | 2023.02.09 |