npm i -g @nestjs/cli
원하는 폴더로 이동 후
nest new
프로젝트 명 설정해주면 된다. (hi-nest)
깃헙 리포지토리를 만들고 vscode에서 git remote add origin 해준다.
src폴더의 ~.sprc
파일을 지워준다.
npm run start:dev
를하면 애플리케이션이 시작된다.
localhost:3000 을 가보면 hello world가 나온다.
main.ts가 무조건 있어야한다. 이름을 바꾸면 안된다.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
async await 빼고는 특별한게 없다. 함수 명은 바꿔도 된다. AppModule에 가보자.
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
데코레이터가 사용되었다. 데이코레이터를 통해 클래스에 함수 기능을 추가할 수 있다.
AppController로 가보자.
AppController
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
}
AppService가 들어있다.
AppService
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
앱 모듈은 모든 것의 루트 모듈 같은 것이다. 모듈은 애플리케이션의 일부다.
컨트롤러는 기본적으로 url을 가져오고 함수를 실행한다. express의 라우터와 같은 역할이다.
app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getHello(): string {
return this.appService.getHello();
}
@Get('/hello')
sayHello(): string {
return 'Hello everyone';
}
}
app.service.ts
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
getHi(): string {
return 'hi nest!';
}
}
nest g co
nest generate controller의 줄임말이다. 이 명령어를 터미널에서 치면 controller 이름을 묻는다. 이름을 입력하면 컨트롤러가 생성된다.
app.module.ts
import { Module } from '@nestjs/common';
import { MoviesController } from './movies/movies.controller';
@Module({
imports: [],
controllers: [MoviesController],
providers: [],
})
export class AppModule {}
movies.controllers.ts
import { Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
@Controller('movies')
export class MoviesController {
@Get()
getAll() {
return 'This will return all movies';
}
@Get("/:id")
getOne(@Param('id') id: string) {
return `This will return one movie with the id: ${id}`;
}
@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")
patch(@Param('id') movieId: string) {
return `This will patch a movie with the id: ${movieId}`;
}
}
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
@Controller('movies')
export class MoviesController {
@Get()
getAll() {
return 'This will return all movies';
}
@Get("search")
search(@Query("year") searchingYear: string) {
return `We are seraching for a movie made after: ${searchingYear}: `;
}
@Get("/:id")
getOne(@Param('id') id: string) {
return `This will return one movie with the id: ${id}`;
}
@Post()
create(@Body() movieData) {
console.log(movieData);
return movieData;
}
@Delete("/:id")
remove(@Param('id') movieId:string) {
return `This will delete a movie with the id: ${movieId}`;
}
@Patch("/:id")
patch(@Param('id') movieId: string, @Body() updateData) {
console.log(updateData);
return {
updateMovie: movieId,
...updateData,
};
}
}
search 부분이 get보다 밑에 있으면 NestJS는 search를 id로 판단한다.
@Get("/:id") getOne(@Param('id') id: string) { return `This will return one movie with the id: ${id}`; }
이 부분이 위에 있으면 아래 부분이 제대로 작동안할 수도 있다. search를 id라 생각하는 것이다.
nest g s
서비스를 생성하는 명령어다.
app.module.ts
import { Module } from '@nestjs/common';
import { MoviesController } from './movies/movies.controller';
import { MoviesService } from './movies/movies.service';
@Module({
imports: [],
controllers: [MoviesController],
providers: [MoviesService],
})
export class AppModule {}
movies.entity.ts
export class Movie {
id: number;
title: string;
year: number;
genres: string[];
}
movies.services.ts
import { Injectable } from '@nestjs/common';
import { Movie } from './movie.entity';
@Injectable()
export class MoviesService {
private movies: Movie[] = [];
getAll(): Movie[] {
return this.movies;
}
getOne(id: string): Movie {
return this.movies.find(movie => movie.id === +id);
}
deleteOne(id:string): boolean {
this.movies.filter(movie => movie.id !== +id);
return true;
}
create(movieData) {
this.movies.push({
id: this.movies.length + 1,
...movieData,
});
}
}
string에 +를 붙이면 number형으로 캐스팅된다.
movies.controller.ts
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
import { Movie } from './movie.entity';
import { MoviesService } from './movies.service';
@Controller('movies')
export class MoviesController {
constructor(private readonly moviesService: MoviesService) {}
@Get()
getAll(): Movie[]{
return this.moviesService.getAll();
}
@Get("/:id")
getOne(@Param('id') id: string): Movie {
return this.moviesService.getOne(id);
}
@Post()
create(@Body() movieData) {
return this.moviesService.create(movieData);
}
@Delete("/:id")
remove(@Param('id') movieId:string) {
return this.moviesService.deleteOne(movieId);
}
@Patch("/:id")
patch(@Param('id') movieId: string, @Body() updateData) {
console.log(updateData);
return {
updateMovie: movieId,
...updateData,
};
}
}
movies.controllers.ts
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
import { Movie } from './movie.entity';
import { MoviesService } from './movies.service';
@Controller('movies')
export class MoviesController {
constructor(private readonly moviesService: MoviesService) {}
@Get()
getAll(): Movie[]{
return this.moviesService.getAll();
}
@Get("/:id")
getOne(@Param('id') id: string): Movie {
return this.moviesService.getOne(id);
}
@Post()
create(@Body() movieData) {
return this.moviesService.create(movieData);
}
@Delete("/:id")
remove(@Param('id') movieId:string) {
return this.moviesService.deleteOne(movieId);
}
@Patch("/:id")
patch(@Param('id') movieId: string, @Body() updateData) {
return this.moviesService.update(movieId, updateData);
}
}
movies.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { Movie } from './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 === +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});
}
}
create-movie.dto.ts
export class CreateMovieDto {
readonly title: string;
readonly year: number;
readonly genres: string[];
}
movies.controller.ts
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
import { CreateMovieDto } from './dto/create-movie.dto';
import { Movie } from './movie.entity';
import { MoviesService } from './movies.service';
@Controller('movies')
export class MoviesController {
constructor(private readonly moviesService: MoviesService) {}
...
@Post()
create(@Body() movieData: CreateMovieDto) {
return this.moviesService.create(movieData);
}
...
}
movies.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateMovieDto } from './dto/create-movie.dto';
import { Movie } from './movie.entity';
@Injectable()
export class MoviesService {
private movies: Movie[] = [];
...
create(movieData: CreateMovieDto) {
this.movies.push({
id: this.movies.length + 1,
...movieData,
});
}
...
}
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({
whitelist:true,
forbidNonWhitelisted: true,
transform: true
}));
await app.listen(3000);
}
bootstrap();
whitelist 옵션은 데코레이터가 없는 프로퍼티를 가진 것을 거르는 것이다.
forbidNonWhitelisted는 아예 요청을 못보내게 막는것이다.
transform은 자료형을 변환해주는 것이다. 처음에는 다 string이다.
유효성 검증을 위해 pipe를 main.ts에 둘 것이다. express에서 미들웨어와 같고, 스프링에서는 인터셉터와 같은 것 같다.
npm i class-validator class-transformer
create-movies.dto.ts
import { IsNumber, IsString } from "class-validator";
export class CreateMovieDto {
@IsString()
readonly title: string;
@IsNumber()
readonly year: number;
@IsString({each: true})
readonly genres: string[];
}
npm i @nestjs/mapped-types
https://docs.nestjs.com/openapi/mapped-types
스프링에서 ObjectMapper 역할을 하는 것 같다?
update-movie.dto.ts
import { PartialType } from "@nestjs/mapped-types";
import { IsNumber, IsString } from "class-validator";
import { CreateMovieDto } from "./create-movie.dto";
export class UpdateMovieDto extends PartialType(CreateMovieDto) {
}
updateMovieDto는 CreateMovieDto와 비슷하다. 다만 차이점은 구성 요소가 필수가 아니다.
app.module은 사실 AppContrller랑 AppProvider만 가지고 있어야 한다. MovieService랑 MoviesController를 movies.module로 옮긴다.
nest g mo
module 생성. 이름은 movies로 했다.
app.module.ts
import { Module } from '@nestjs/common';
import { MoviesModule } from './movies/movies.module';
@Module({
imports: [MoviesModule],
controllers: [],
providers: [],
})
export class AppModule {}
movies.module.ts
import { Module } from '@nestjs/common';
import { MoviesController } from './movies.controller';
import { MoviesService } from './movies.service';
@Module({
controllers: [MoviesController],
providers: [MoviesService],
})
export class MoviesModule {}
MovieController에서 MovieService를 사용하고 있는데 DI다. 스프링이랑 똑같다.
NestJS는 Express 위에서 돌아간다. 또한 Fastify라는 프레임워크 위에서 돌아가기도 하므로 Express에 의존적으로 코드를 짜면 안된다.
jest는 자스용 테스트 도구다. .spec.ts
로 끝나는 파일은 테스트를 포함한 파일이다.
movies.controller.ts
를 테스트하고 싶으면 movies.controller.spec.ts
라는 파일이 있어야 한다. jest가 .spec.ts
를 찾는다.
npm run test:cov
현재 얼마나 테스트 코드가 이뤄졌나 보여준다.
npm run test:watch
유닛테스트는 서비스에서 분리된 유닛을 테스트하고, e2e 테스트는 이건 모든 시스템을 테스트하는 것이다. 이 페이지로 가면 특정 페이지가 나와야하는 경우 사용한다. 사용자 스토리랑 비슷하다.
movies.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { MoviesService } from './movies.service';
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();
});
it("should be 4", () => {
expect(2+2).toEqual(4);
})
});
movies.service.spec.ts
import { NotFoundException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { MoviesService } from './movies.service';
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",
genres: ["Test"],
year: 2000,
});
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.");
}
})
});
});
movies.service.spec.ts
import { NotFoundException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { MoviesService } from './movies.service';
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",
genres: ["Test"],
year: 2000,
});
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",
genres: ["Test"],
year: 2000,
});
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);
expect(e.message).toEqual("Movie with ID 999 not found.");
}
})
})
describe("create", () => {
it("should create a movie", () => {
const beforeCreate = service.getAll().length;
service.create({
title: "Test Movie",
genres: ["Test"],
year: 2000,
});
const afterCreate = service.getAll().length;
expect(afterCreate).toBeGreaterThan(beforeCreate);
});
})
});
movies.service.spec.ts
...
describe("update", () => {
it("should update a movie", () => {
service.create({
title: "Test Movie",
genres: ["Test"],
year: 2000,
});
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);
expect(e.message).toEqual("Movie with ID 999 not found.");
}
})
})
});
npm run test:cov
를 통해서 얼마나 진행됐는지 확인 가능
~.spec.ts
는 유닛테스트용 파일이다.
npm run test:e2e
movies.service.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Welcom to my Movie API');
});
describe('/movies', () => {
it('GET', () => {
return request(app.getHttpServer())
.get('/movies')
.expect(200)
.expect([]);
});
it('POST', () => {
return request(app.getHttpServer())
.post('/movies')
.send({
title: 'Test',
year: 2000,
genres: ['test'],
})
.expect(201);
});
it('DELETE', () => {
return request(app.getHttpServer())
.delete('/movies')
.expect(404);
});
});
});
위의 코드로는 매 테스트를 진행할때마다 새로운 어플리케이션을 만드는 것이다. 그래서 데이터베이스가 계속 비어있다. 그래서 beforeAll로 바꿔준다.
테스트 서버에서는 get해서 id의 타입을 찍어보면 string이다. transform을 테스트가 아닌 실제 애플리케이션에만 걸어줬다. 테스트에도 실제 어플리케이션 환경을 그대로 적용시켜줘야 한다.
app.e2e-spec.ts
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 () => {
...
app.useGlobalPipes(new ValidationPipe({
whitelist:true,
forbidNonWhitelisted: true,
transform: true
}),
);
await app.init();
});
...
describe('/movies/:id', () => {
it('GET 200', () => {
return request(app.getHttpServer()).get("/movies/1").expect(200);
});
})
});
movies.service.spec.ts
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,
transform: true
}),
);
await app.init();
});
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Welcom 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', () => {
return request(app.getHttpServer())
.delete('/movies/1')
.expect(200);
});
})
});