목차
main.ts
•
NestJS는 main.ts 파일을 가지며 무조건 main이라는 이름이어야 한다
Decorator
•
클래스에 함수 기능을 추가하는 것
•
클래스 위의 함수이며, 클래스를 위해 움직임
Architecture
•
main.ts에서 NestJS 애플리케이션이 시작함
•
NestJS에서는 컨트롤러와 비즈니스 로직을 구분함
◦
컨트롤러는 url을 가져오는 역할, function을 실행하는 역할
◦
서비스는 비즈니스 로직을 처리함
▪
실제 function을 가지는 부분
Module
•
하나의 모듈에서 애플리케이션을 생성
◦
앱 모듈은 모든 모듈의 루트
◦
모듈: 애플리케이션의 일부분, 한 가지의 역할을 하는 앱
▪
ex) Users module - 인증을 담당하는 애플리케이션
Controller
•
컨트롤러가 하는 일은 기본적으로 url을 가져오고 함수를 실행함
◦
함수가 놓이는 곳은 Service
•
express의 router와 같음
@Get("/hello")
sayHello(): string {
return "Hello everyone"
}
TypeScript
복사
•
위와 같이 사용해도 브라우저에서 작동하는데 왜 서비스를 사용해야 하는가?
Service
•
비즈니스 로직을 처리하며, 실제 function을 가지는 부분
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHi(): string {
return 'Hello everyone';
}
}
TypeScript
복사
@Get("/hello")
sayHello(): string {
return this.appService.getHi();
}
TypeScript
복사
Rest API 만들기
Controller 생성
•
nest g co = nest generate controller
•
컨트롤러를 생성하면 모듈에도 자동 참조됨
CRUD
import { Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common';
@Controller('movies') // url의 Entry Point를 컨트롤 함
export class MoviesController {
@Get()
getAll() {
return "This will return all movies";
}
@Get("/:id")
getOne(@Param("id") movieId: string) {
return `This will return one movie with the id: ${movieId}`;
}
@Post()
create() {
return 'This will create a movie';
}
@Delete(":id")
remove(@Param("id") movieId:string) {
return `This will delete a movie with the id: ${movieId}`;
}
@Patch('/:id') // Put은 모든 리소스를 업데이트. patch는 리소스의 일부분만 업데이트
path(@Param('id') movieId: string) {
return `This will patch a movie with the id: ${movieId}`;
}
}
TypeScript
복사
Service 생성
•
nest g s = nest generate service
Service
import { Injectable, NotFoundException } from '@nestjs/common';
import { Movie } from './entities/movie.entity';
@Injectable()
export class MoviesService {
private movies: Movie[] = [];
getAll(): Movie[] {
return this.movies;
}
getOne(id:string):Movie {
const movie = this.movies.find(movie => movie.id === parseInt(id)); // parseInt(id) = +id
if (!movie) {
throw new NotFoundException(`Movie with ID ${id} not found.`);
}
return movie;
}
deleteOne(id:string) {
this.getOne(id)
this.movies = this.movies.filter(movie => movie.id !== +id);
}
create(movieData){
this.movies.push({
id: this.movies.length + 1,
...movieData
})
}
update(id:string, updateData) {
const movie = this.getOne(id);
this.deleteOne(id);
this.movies.push({...movie, ...updateData})
}
}
TypeScript
복사
Service에 맞게 Controller 수정
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common';
import { MoviesService } from './movies.service';
import { Movie } from './entities/movie.entity';
@Controller('movies') // url의 Entry Point를 컨트롤 함
export class MoviesController {
constructor(private readonly moviesService: MoviesService) {}
@Get()
getAll(): Movie[] {
return this.moviesService.getAll();
}
@Get("/:id")
getOne(@Param("id") movieId: string): Movie {
return this.moviesService.getOne(movieId);
}
@Post()
create(@Body() movieData) {
return this.moviesService.create(movieData);
}
@Delete(":id")
remove(@Param("id") movieId:string) {
return this.moviesService.deleteOne(movieId);
}
@Patch('/:id') // Put은 모든 리소스를 업데이트. patch는 리소스의 일부분만 업데이트
patch(@Param('id') movieId: string, @Body() updateData) {
return this.moviesService.update(movieId, updateData)
}
}
TypeScript
복사
DTO
•
Data Transfer Object ( 데이터 전송 객체 )
•
DTO를 통해서 유효성 검사를 함
Create DTO
import { IsNumber, IsString } from "class-validator";
export class CreateMovieDto {
@IsString()
readonly title: string;
@IsNumber()
readonly year: number;
@IsString({ each: true })
readonly genres: string[];
}
TypeScript
복사
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // 데코레이터가 없는 속성은 제거함
forbidNonWhitelisted: true, // 화이트리스트에 존재하지 않는 데이터가 있을 시 HttpException. whitelist: true가 선행되어야 함
transform: true, // transform은 사용자의 값을 실제 타입으로 변환시켜줌 -> url은 전부 string이기 때문에 기존에는 id를 string으로 받아 int 타입으로 변환시켰음
}));
await app.listen(3000);
}
bootstrap();
TypeScript
복사
Update DTO
•
모든 필드가 필수가 아니기 때문에 {변수}? 로 사용할 수 있음
•
다른 방법으로는 PartialType을 사용할 수 있음
import { IsNumber, IsString } from "class-validator";
import { PartialType } from "@nestjs/mapped-types"
import { CreateMovieDto } from "./create-movie.dto";
// export class UpdateMovieDto {
// @IsString()
// readonly title?: string;
// @IsNumber()
// readonly year?: number;
// @IsString({ each: true })
// readonly genres?: string[];
// }
export class UpdateMovieDto extends PartialType(CreateMovieDto) {}
TypeScript
복사
Module
app.module
•
app.module은 AppService와 AppController만 가져야 함
•
여러 모듈을 app.module로 불러와 사용하는 것임
import { Module } from '@nestjs/common';
import { MoviesModule } from './movies/movies.module';
import { AppController } from './app.controller';
@Module({
imports: [MoviesModule],
controllers: [AppController], // app.module은 AppController와 AppService만 가짐
providers: [],
})
export class AppModule {}
TypeScript
복사
import { Module } from '@nestjs/common';
import { MoviesController } from './movies.controller';
import { MoviesService } from './movies.service';
@Module({
controllers: [MoviesController],
providers: [MoviesService]
})
export class MoviesModule {}
TypeScript
복사
Testing
NestJS Test
jest
•
자바스크립트 테스팅 프레임워크
•
JS를 테스팅하는 npm 패키지
•
NestJS에서는 jest가 .spec.ts 파일을 찾아 볼 수 있도록 설정됨
.spec.ts
•
테스트를 포함한 파일
cov
•
코드가 얼마나 테스팅되었는지 또는 안 됐는지 알려줌
•
모든 .spec.ts 파일을 찾아 몇 줄이 테스팅되었는지 알려줌
Unit Test
•
모든 function을 따로 테스트하는 기법
•
서비스에서 분리된 유닛을 테스트
import { Test, TestingModule } from '@nestjs/testing';
import { MoviesService } from './movies.service';
import { NotFoundException } from '@nestjs/common';
describe('MoviesService', () => {
let service: MoviesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [MoviesService],
}).compile();
service = module.get<MoviesService>(MoviesService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe("getAll", () => {
it('should return an array', () => {
const result = service.getAll()
expect(result).toBeInstanceOf(Array);
})
})
describe("getOne", () => {
it('should return a movie', () => {
service.create({
title: "Test Movie",
year: 2000,
genres: ['test'],
});
const movie = service.getOne(1);
expect(movie).toBeDefined();
// expect(movie.id).toEqual(1);
});
it('should throw 404 error', () => {
try {
service.getOne(999);
} catch (e) {
expect(e).toBeInstanceOf(NotFoundException);
// expect(e.message).toEqual('Movie with ID 999 not found.')
}
})
});
describe("deleteOne", () => {
it('deletes a movie', () => {
service.create({
title: "Test Movie",
year: 2000,
genres: ['test'],
});
const beforeDelete = service.getAll();
service.deleteOne(1);
const afterDelete = service.getAll();
expect(afterDelete.length).toBeLessThan(beforeDelete.length);
});
it('should return a 404', () => {
try {
service.deleteOne(999);
} catch (e) {
expect(e).toBeInstanceOf(NotFoundException);
}
});
});
describe('create', () => {
it('should create a movie', () => {
const beforeCreate = service.getAll().length
service.create({
title: "Test Movie",
year: 2000,
genres: ['test'],
});
const afterCreate = service.getAll().length
expect(afterCreate).toBeGreaterThan(beforeCreate)
});
});
describe('update', () => {
it('should update a movie', () => {
service.create({
title: "Test Movie",
year: 2000,
genres: ['test'],
});
service.update(1, { title: "Updated Test" });
const movie = service.getOne(1);
expect(movie.title).toEqual('Updated Test');
});
it('should throw a NotFoundException', () => {
try {
service.update(999, {});
} catch (e) {
expect(e).toBeInstanceOf(NotFoundException);
}
});
});
});
TypeScript
복사
E2E Test (end-to-end)
•
모든 시스템을 테스팅
•
사용자 관점 테스트
•
테스트를 위해 test 폴더가 필요
•
NestJS는 테스트마다 애플리케이션을 생성함
◦
브라우저에서 확인하는 애플리케이션과는 다른 것임
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
TypeScript
복사
◦
테스트 할때마다 애플리케이션을 생성하면 데이터베이스가 빈 상태가 됨
◦
따라서 테스팅을 시작하기 전에 애플리케이션을 새로 만들기 위해 beforeAll 사용
•
테스트에서도 실제 애플리케이션의 환경을 그대로 적용시켜줘야 함
◦
main.ts에서 transform을 통해 url에서의 숫자를 string이 아닌 number로 변환함
◦
테스트에서는 적용되지 않기 때문에 따로 적용시켜줘야 함
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // 데코레이터가 없는 속성은 제거함
forbidNonWhitelisted: true, // 화이트리스트에 존재하지 않는 데이터가 있을 시 HttpException. whitelist: true가 선행되어야 함
transform: true, // transform은 사용자의 값을 실제 타입으로 변환시켜줌 -> url은 전부 string이기 때문에 기존에는 id를 string으로 받아 int 타입으로 변환시켰음
}));
await app.init();
});
TypeScript
복사
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // 데코레이터가 없는 속성은 제거함
forbidNonWhitelisted: true, // 화이트리스트에 존재하지 않는 데이터가 있을 시 HttpException. whitelist: true가 선행되어야 함
transform: true, // transform은 사용자의 값을 실제 타입으로 변환시켜줌 -> url은 전부 string이기 때문에 기존에는 id를 string으로 받아 int 타입으로 변환시켰음
}));
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Welcome to my Movie API');
});
describe("/movies", () => {
it('GET', () => {
return request(app.getHttpServer())
.get('/movies')
.expect(200)
.expect([]);
});
it('POST 201', () => {
return request(app.getHttpServer())
.post('/movies')
.send({
title: "Test",
year: 2000,
genres: ['test']
})
.expect(201);
});
it('POST 400', () => {
return request(app.getHttpServer())
.post('/movies')
.send({
title: "Test",
year: 2000,
genres: ['test'],
other: 'thing',
})
.expect(400);
});
it('DELETE', () => {
return request(app.getHttpServer())
.delete('/movies')
.expect(404);
})
});
describe('/movies/:id', () => {
it('GET 200', () => {
return request(app.getHttpServer())
.get('/movies/1')
.expect(200)
});
it('GET 404', () => {
return request(app.getHttpServer())
.get('/movies/999')
.expect(404)
})
it("PATCH 200", () => {
return request(app.getHttpServer())
.patch('/movies/1')
.send({ title: 'Updated Test' })
.expect(200);
});
it("DELETE 200", () => {
return request(app.getHttpServer())
.delete('/movies/1')
.expect(200)
});
})
});
TypeScript
복사