nest 공식 강의를 참고하였으며,
소스코드는 https://github.com/jujube0/nest-study 에서 확인 가능하다.
새로운 프로젝트 만들기 : nest new {project-name}
모듈 생성하기 : 터미널에서 nest g module user
nest g controller user
), service(nest g service user
)도 만들 수 있다.또는
nest g resource [name]
를 이용하면 기본적인 CRUD가 존재하는 폴더가 생성된다.
$npm install --save @types/express
- type definition for express
import { Controller, Get } from '@nestjs/common';
@Controller('user')
export class UserController {
@Get()
findAll(): string {
return 'returns all users';
}
}
@Controller('user')
: /user
의 path prefix를 갖도록 한다.@Get()
: HTTP request method decorator,/user
뒤에 올 route path를 추가할 수 있다.@Get('profile')
을 접근하기 위해서는 GET /user/profile
로 접근한다.Manipulating Responses
built-in method를 통해 object나 array를 리턴하면 JSON으로 바꾸어 보내준다. status code는 200(POST는 201)이 자동으로 설정된다.@HttpCode(...)
데코레이터를 이용하여 변경할 수 있다.
Library-specific
express 등의 라이브러리를 이용할 수 있다. method handler의 input으로@Res
데코레이터를 넣어줄 수 있다(e.g.,findAll(@Res() response))
.
전송은response.status(200).send()
handler signature에 @Req()
를 추가한다.
import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';
@Controller('user')
export class UserController {
@Get()
findAll(@Req() request: Request): string {
console.log(request);
return 'returns all users';
}
}
@Get()
findAll(@Query() paginationQuery) {
const { limit, offset } = paginationQuery;
return `This action returns all coffees Limit: ${limit}, offset: ${offset}`;
}
request object는 request query string, parameter, HTTP headers, body property를 가진다.
@Query(key?: string)
, @Param(key?: string)
, @Body(key?: string)
등의 decorator로 property에 직접 접근도 가능하다.
POST
를 사용하는 방법은 다음과 같다.import { Controller, Get, Post, Req } from '@nestjs/common';
import { Request } from 'express';
@Controller('user')
export class UserController {
...
@Post()
create(): string {
return 'This action adds a new user';
}
}
@Get()
, @Post()
, @Put()
, 등의 HTTP Method를 이용할 수 있다. @All()
은 모든 것들을 다루는 endpoint를 생성한다.@Post()
@HttpCode(204)
create() {
return 'This action adds a new user';
}
@nestjs/common
package@Res
를 이용한 library-specific method를 이용하자.@Get(':id')
findOne(@Param() params): string {
console.log(params.id);
return `This action returns a #${params.id} user`;
}
@Get('profile/:id')
findProfile(@Param('id') id: string): string {
console.log(id);
return `This action returns a #${id} profile`;
}
@Get()
async findAll(): Promise<any[]> {
return [];
}
@Body()
데코레이터를 이용하여 POST route가 클라이언트가 보낸 request body를 읽을 수 있도록 만들어보자.
일단 DTO(Data Transfer Object)를 만들어줘야 한다. 클래스나 인터페이스를 이용하여 만들 수 있는데, 클래스가 선호된다(클래스는 런타임에도 존재하지만 인터페이스는 제거됨).
cli nest generate class coffees/dto/create-coffee.dto --no-spec
export class CreatePostDto {
title: string;
content: string;
}
@Post()
create(@Body() createPostDto: CreatePostDto): string {
return 'This action adds a new post';
}
import { Body, Controller, Get, HttpStatus, Post, Res } from '@nestjs/common';
import { CreatePostDto } from './create-post.dto';
import { Response } from 'express';
@Controller('post')
export class PostController {
// library specific approach
@Get()
findAll(@Res() res: Response) {
res.status(HttpStatus.CREATED).send();
}
@Post()
create(@Body() createPostDto: CreatePostDto, @Res() res: Response) {
res.status(HttpStatus.OK).json(createPostDto);
}
}
@HttpCode()
, @Header()
, interceptors)을 이용할 수 없음 → 이를 고치기 위해서는 @Res({ passthrough: true})
를 추가해준다.-> it can inject dependency
nestjs/common을 이용하자.
const coffee = this.coffees.find(item => item.id === +id);
if (!coffee) {
throw new HttpException(`Coffee #${id} not found`, HttpStatus.NOT_FOUND);
}
return coffee;
if (!coffee) {
throw new NotFoundException(`Coffee #${id} not found`);
}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap()
에 app.useGlobalPipe()
를 추가해준다.
npm i class-validator class-transformer
import { IsString } from "class-validator";
export class CreateCoffeeDto {
@IsString()
readonly name: string;
@IsString()
readonly brand: string;
@IsString({ each: true })
readonly flavors: string[];
}
이제 정상적인 Request body를 보내지 않으면 다음과 같은 json을 리턴한다.
{
"statusCode": 400,
"message": [
"name must be a string",
"each value in flavors must be a string"
],
"error": "Bad Request"
}
npm i @nestjs/mapped-types
import { PartialType } from "@nestjs/mapped-types";
import { CreateCoffeeDto } from "./create-coffee.dto";
export class UpdateCoffeeDto extends PartialType(CreateCoffeeDto){ }
version: "3"
services:
db:
image: postgres
restart: always
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: pass123
// Start containers in detached / background mode
docker-compose up -d
// Stop containers
docker-compose down
event.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Event {
@PrimaryGeneratedColumn()
id: number;
@Column()
type: string;
@Column()
name: string;
@Column('json')
payload: Record<string, any>;
}
@Injectable()
export class CoffeesService {
constructor(
...
private readonly connection: Connection,
) {}
...
async recommendCoffee(coffee: Coffee) {
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
coffee.recommendations++;
const recommendEvent = new Event();
recommendEvent.name = 'recommend_coffee';
recommendEvent.type = 'coffee';
recommendEvent.payload = { coffeeId: coffee.id };
await queryRunner.manager.save(coffee);
await queryRunner.manager.save(recommendEvent);
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
} finally {
await queryRunner.release();
}
}
}
ormconfig.js 파일 만든 후,
shell에서
npx typeorm migration:create -n CoffeeRefactor
-> src/migration 에 migration file을 만들어줌
coffee entity의
name
칼럼을title
로 바꾸는 상황을 가정해보자.
단순히 name을 title로 바꿔버린다면, name에 있던 데이터들은 모두 날아가버릴 것이다.
migrations file을 바꿔준다.
import {MigrationInterface, QueryRunner} from "typeorm";
export class CoffeeRefactor1626325978666 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> { // what needs to be changed and how
await queryRunner.query(
`ALTER TABLE "coffee" RENAME COLUMN "name" TO "title"`
)
}
public async down(queryRunner: QueryRunner): Promise<void> { // undo or roll back
await queryRunner.query(
`ALTER TABLE "coffee" RENAME COLUMN "title" TO "name"`
)
}
}
num run build
npx typeorm migration:run
(npx typeorm migration:revert
)
typeorm은 db의 entity와 현재 entity 파일을 비교하여 직접 migration 파일을 만들게 할 수도 있다.
npm run build
npx typeorm migration:generate -n SchemaSync
npx typeorm migration:run
CoffeesService
export@Module({
...
exports: [CoffeesService],
})
CoffeesModule
importimport { Module } from '@nestjs/common';
import { CoffeesModule } from 'src/coffees/coffees.module';
import { CoffeeRatingService } from './coffee-rating.service';
@Module({
imports: [CoffeesModule],
providers: [CoffeeRatingService]
})
export class CoffeeRatingModule {}
CoffeesService
importimport { Injectable } from '@nestjs/common';
import { CoffeesService } from 'src/coffees/coffees.service';
@Injectable()
export class CoffeeRatingService {
constructor(private readonly coffeeService: CoffeesService) {}
}
모든 module은 provider들을 encapsulate한다 → 다른 module에서 이를 이용하려면 exported
에 직접 명시해주어 API에 포함시켜줘야한다.
좀 더 복잡한 provider를 이용할 때를 생각해보자.
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeesController],
providers: [{ provide : CoffeesService, useValue: CoffeesService}],
exports: [CoffeesService],
})
실제 provider 구조는 다음과 같다. 우리가 쓰는 것은 shorthand
그래서
class MockCoffeeService {}
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeesController],
providers: [{ provide : CoffeesService, useValue: new MockCoffeeService()}],
exports: [CoffeesService],
})
export class CoffeesModule {}
이렇게 이용할 수 있다는 말.
@Module({
...
providers: [CoffeesService, { provide: 'COFFEE_BRANDS', useValue: ['buddy brew', 'nescafe']}],
})
이용할 때에는
@Injectable()
export class CoffeesService {
constructor(
...
@Inject('COFFEE_BRANDS') coffeeBrands: string[],
) {}
@Inject()
데코레이터 안에 TOKEN을 넣으면 된다.
typo 오류 등을 방지하기 위해서 TOKEN들을 다른 파일에 저장해 놓는 것이 좋다.
providers: [
CoffeesService,
{
provide: ConfigService,
useClass: process.env.NODE_ENV === 'development' ? DevelopmentConfigService: ProductionConfigService
}
],
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event])],
controllers: [CoffeesController],
providers: [
CoffeesService,
CoffeeBrandsFactory,
{
provide: COFFEE_BRANDS,
useFactory: (brandsFactory: CoffeeBrandsFactory) => brandsFactory.create(),
inject: [CoffeeBrandsFactory]
}
],
exports: [CoffeesService],
})
// Asynchronous "useFactory" (async provider example)
{
provide: 'COFFEE_BRANDS',
// Note "async" here, and Promise/Async event inside the Factory function
// Could be a database connection / API call / etc
// In our case we're just "mocking" this type of event with a Promise
useFactory: async (connection: Connection): Promise<string[]> => {
// const coffeeBrands = await connection.query('SELECT * ...');
const coffeeBrands = await Promise.resolve(['buddy brew', 'nescafe'])
return coffeeBrands;
},
inject: [Connection],
},
해당 provider에 의존하는 클래스를 instantiate하기 전에 위 promise를 먼저 resolve하게 된다.
위에서 다룬 모듈들은 static module이었다.
static modules can't have their providers be configured by a module that is consuming it
// Generate a DatabaseModule
nest g mo database
// Initial attempt at creating "CONNECTION" provider, and utilizing useValue for values */
{
provide: 'CONNECTION',
useValue: createConnection({
type: 'postgres',
host: 'localhost',
port: 5432
}),
}
// Creating static register() method on DatabaseModule
export class DatabaseModule {
static register(options: ConnectionOptions): DynamicModule { }
}
// Improved Dynamic Module way of creating CONNECTION provider
export class DatabaseModule {
static register(options: ConnectionOptions): DynamicModule {
return {
module: DatabaseModule,
providers: [
{
provide: 'CONNECTION', // 👈
useValue: createConnection(options),
}
]
}
}
}
// Utilizing the dynamic DatabaseModule in another Modules imports: []
imports: [
DatabaseModule.register({ // 👈 passing in dynamic values
type: 'postgres',
host: 'localhost',
password: 'password',
})
]
@Injectable({ scope: Scope.DEFAULT })
export class CoffeesService {
}
bootstrap()
되면,모든 singleton provider가 instantiate됨.두가지 scope option이 있다.
1. Transient
@Injectable({ scope: Scope.TRANSIENT })
export class CoffeesService {
TRANSIENT
로 바꾸면, 해당 provider를 inject하는 컨수머들은 모두 새로운 인스턴스를 갖게된다.npm run start
를 해도 instance 가 만들어지지 않는다. // Injecting the ORIGINAL Request object
@Injectable({ scope: Scope.REQUEST })
export class CoffeesService {
constructor(@Inject(REQUEST) private request: Request) {} // 👈
}
npm i @nestjs/config
.env
파일을 만들어주자.TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
autoLoadEntities: true,
synchronize: true,
}),
.env
파일을 찾는다. 이를 바꿔주려면?ConfigModule.forRoot({
envFilePath: '.environment’,
});
ignoreEnvFile: true
를 옵션으로 넣어주자.// Install neccessary dependencies
$ npm install @hapi/joi
$ npm install --save-dev @types/hapi__joi
// Use Joi validation
ConfigModule.forRoot({
validationSchema: Joi.object({
DATABASE_HOST: Joi.required(),
DATABASE_PORT: Joi.number().default(5432),
}),
}),
Joi.required()
를 이용하여 DATABASE_HOST
를 필수로 만들었다.Joi.number
로 number로 parsable한 조건을 추가하고, 디폴트 값을 5432로 설정했다.get()
@Module({
imports: [TypeOrmModule.forFeature([Coffee, Flavor, Event]), ConfigModule],
...
})
/* Utilize ConfigService */
import { ConfigService } from '@nestjs/config';
constructor(
private readonly configService: ConfigService, // 👈
) {}
/* Accessing process.env variables from ConfigService */
const databaseHost = this.configService.get<string>('DATABASE_HOST');
console.log(databaseHost);
/* /src/config/app.config.ts File */
export default () => ({
environment: process.env.NODE_ENV || 'development',
database: {
host: process.env.DATABASE_HOST,
port: parseInt(process.env.DATABASE_PORT, 10) || 5432
}
});
/* Setting up "appConfig" within our Application */
import appConfig from './config/app.config';
@Module({
imports: [
ConfigModule.forRoot({
load: [appConfig], // 👈
}),
],
})
export class AppModule {}
// ---------
/**
* Grabbing this nested property within our App
* via "dot notation" (a.b)
*/
const databaseHost = this.configService.get('database.host', 'localhost');
load
옵션을 추가해준다./* /src/coffees/coffees.config.ts File */
export default registerAs('coffees', () => ({ // 👈
foo: 'bar', // 👈
}));
/* Partial Registration of coffees namespaced configuration */
@Module({
imports: [ConfigModule.forFeature(coffeesConfig)], // 👈
})
export class CoffeesModule {}
// ---------
// ⚠️ sub optimal ways of retrieving Config ⚠️
/* Grab coffees config within App */
const coffeesConfig = this.configService.get('coffees');
console.log(coffeesConfig);
/* Grab nested property within coffees config */
const foo = this.configService.get('coffees.foo');
console.log(foo);
// ---------
// 💡 Optimal / Best-practice 💡
constructor(
@Inject(coffeesConfig.KEY)
private coffeesConfiguration: ConfigType<typeof coffeesConfig>,
) {
// Now strongly typed, and able to access properties via:
console.log(coffeesConfiguration.foo);
}
-> 실제 도메인과 가까운 곳에 config파일을 위치시킬 수 있다.
@Module({
imports: [
ConfigModule.forRoot({
load: [appConfig]
}),
CoffeesModule,
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
autoLoadEntities: true,
synchronize: true,
}),
CoffeeRatingModule,
],
forRootAsync()
method를 이용할 수 있다. /* forRootAsync() */
TypeOrmModule.forRootAsync({ // 👈
useFactory: () => ({
type: 'postgres',
host: process.env.DATABASE_HOST,
port: +process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
autoLoadEntities: true,
synchronize: true,
}),
}),
UsePipes
// Generate Filter with Nest CLI
nest g filter common/filters/http-exception
// Catch decorator
@Catch(HttpException)
/* HttpExceptionFilter final code */
import { Catch, HttpException, ExceptionFilter, ArgumentsHost } from "@nestjs/common";
import { Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter<T extends HttpException> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
const error =
typeof response === 'string'
? { message: exceptionResponse }
: (exceptionResponse as object);
response.status(status).json({
...error,
timestamp: new Date().toISOString(),
});
}
}
request에 permission/roles/ACLs 등이 필요할 때
AUthentication / Authorization
authorization Header에 API_KEY가 존재하는지 확인하고,
접근한 route가 public인지 확인해보자
// Generate ApiKeyGuard with Nest CLI
nest g guard common/guards/api-key
// Apply ApiKeyGuard globally
app.useGlobalGuards(new ApiKeyGuard());
/* ApiKeyGuard code */
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Request } from 'express';
import { Observable } from 'rxjs';
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest<Request>();
const authHeader = request.header('Authorization');
return authHeader === process.env.API_KEY;
}
}
canActivate
: 현 request가 허용되었는지의 여부를 boolean으로 리턴한다.@SetMetadata('key', 'value');
/* public.decorator.ts FINAL CODE */
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
/* ApiKeyGuard FINAL CODE */
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { Observable } from 'rxjs';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(
private readonly reflector: Reflector,
private readonly configService: ConfigService,
) {}
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const isPublic = this.reflector.get(IS_PUBLIC_KEY, context.getHandler());
if (isPublic) {
return true;
}
const request = context.switchToHttp().getRequest<Request>();
const authHeader = request.header('Authorization');
return authHeader === this.configService.get('API_KEY');
}
}
useGlobalGuard
는 다른 모듈에 의존성을 갖지 않을 때만 이용이 가능하다.interceptor : 코드를 수정하지 않으면서 기능을 추가할 수 있다
모든 response에 결과가 data property로 들어가길 원한다고 가정해보자.
rxjs : alternative to Promise or callbacks
// Generate WrapResponseInterceptor with Nest CLI
nest g interceptor common/interceptors/wrap-response
/* WrapResponseInterceptor FINAL CODE */
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class WrapResponseInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
return next.handle().pipe(map(data => ({ data })));
}
}
// Apply Interceptor globally in main.ts file
app.useGlobalInterceptors(new WrapResponseInterceptor());
/* Generate TimeoutInterceptor with Nest CLI */
nest g interceptor common/interceptors/timeout
/* Apply TimeoutInterceptor globally in main.ts file */
app.useGlobalInterceptors(
new WrapResponseInterceptor(),
new TimeoutInterceptor(), // 👈
);
/* Add manual timeout to force timeout interceptor to work */
await new Promise(resolve => setTimeout(resolve, 5000));
/* TimeoutInterceptor FINAL CODE */
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
RequestTimeoutException,
} from '@nestjs/common';
import { Observable, throwError, TimeoutError } from 'rxjs';
import { catchError, timeout } from 'rxjs/operators';
@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
timeout(3000),
catchError(err => {
if (err instanceof TimeoutError) {
return throwError(new RequestTimeoutException());
}
return throwError(err);
}),
);
}
}
Pipes have two typical use cases:
Transformation: where we transform input data to the desired output
& validation: where we evaluate input data and if valid, simply pass it through unchanged. If the data is NOT valid - we want to throw an exception.
In both cases, pipes operate on the arguments being processed by a controller’s route handler.
NestJS triggers a pipe just before a method is invoked.
Pipes also receive the arguments meant to be passed on to the method. Any transformation or validation operation takes place at this time - afterwards the route handler is invoked with any (potentially) transformed arguments.
// Generate ParseIntPipe with Nest CLI
nest g pipe common/pipes/parse-int
/* ParseIntPipe FINAL CODE */
import {
ArgumentMetadata,
BadRequestException,
Injectable,
PipeTransform,
} from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform {
transform(value: string, metadata: ArgumentMetadata) {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException(
`Validation failed. "${val}" is not an integer.`,
);
}
return val;
}
}
Middleware functions have access to the request and response objects, and are not specifically tied to any method, but rather to a specified route PATH.
Middleware functions can perform the following tasks:
When working with middleware, if the current middleware function does not END the request-response cycle, it must call the next() method, which passes control to the next middleware function.
Otherwise, the request will be left hanging - and never complete.
// Generate LoggingMiddleware with Nest CLI
nest g middleware common/middleware/logging
// Apply LoggingMiddleware in our AppModule
consumer
.apply(LoggingMiddleware)
.forRoutes(‘*’);
/* LoggingMiddleware FINAL CODE */
import {
Injectable,
NestMiddleware,
} from '@nestjs/common';
@Injectable()
export class LoggingMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
console.time('Request-response time');
console.log('Hi from middleware!');
res.on('finish', () => console.timeEnd('Request-response time'));
next();
}
}
@Module({
imports: [ConfigModule],
providers: [{ provide: APP_GUARD, useClass: ApiKeyGuard }]
})
export class CommonModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggingMiddleware).forRoutes('*');
//OR
// consumer.apply(LoggingMiddleware).forRoutes({ path: 'coffees', method: RequestMethod.GET});
}
}
// Using the Protocol decorator
@Protocol(/* optional defaultValue */)
/* @Protocal() decorator FINAL CODE */
import {
createParamDecorator,
ExecutionContext,
} from '@nestjs/common';
export const Protocol = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.protocol;
},
);
@Protocol(data)
에 넣은 data는 createParamDecorator의 첫번째 인자가 된다.-> use the OpenAPI specification. The OpenAPI specification is a language-agnostic definition format used to describe RESTful APIs.
An OpenAPI document allows us to describe our entire API, including:
/**
* Installing @nestjs/swagger
* & Swagger UI for Express.js (which our application uses)
* 💡 Note: If your application is using Fastiy, install `fastify-swagger` instead
*/
npm install --save @nestjs/swagger swagger-ui-express
// Setting up Swagger document main.ts bootstrap()
const options = new DocumentBuilder()
.setTitle('Iluvcoffee')
.setDescription('Coffee application')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, options);
SwaggerModule.setup('api', app, document);
/**
* With the App running (npm run start:dev if not)
* To view the Swagger UI go to:
* http://localhost:3000/api
*/
// For unit tests
npm run test
// For unit tests + collecting testing coverage
npm run test:cov
// For e2e tests
npm run test:e2e
*.spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { CoffeesController } from './coffees.controller';
describe('CoffeesController', () => {
let controller: CoffeesController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CoffeesController],
}).compile();
controller = module.get<CoffeesController>(CoffeesController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
describe()
: blockbeforeEach()
: test 전에 할 행동/*
coffees-service.spec.ts - FINAL CODE
*/
import { Test, TestingModule } from '@nestjs/testing';
import { CoffeesService } from './coffees.service';
import { Connection, Repository } from 'typeorm';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Flavor } from './entities/flavor.entity';
import { Coffee } from './entities/coffee.entity';
import { NotFoundException } from '@nestjs/common';
type MockRepository<T = any> = Partial<Record<keyof Repository<T>, jest.Mock>>;
const createMockRepository = <T = any>(): MockRepository<T> => ({
findOne: jest.fn(),
create: jest.fn(),
});
describe('CoffeesService', () => {
let service: CoffeesService;
let coffeeRepository: MockRepository;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CoffeesService,
{ provide: Connection, useValue: {} },
{
provide: getRepositoryToken(Flavor),
useValue: createMockRepository(),
},
{
provide: getRepositoryToken(Coffee),
useValue: createMockRepository(),
},
],
}).compile();
service = module.get<CoffeesService>(CoffeesService);
coffeeRepository = module.get<MockRepository>(getRepositoryToken(Coffee));
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('findOne', () => {
describe('when coffee with ID exists', () => {
it('should return the coffee object', async () => {
const coffeeId = '1';
const expectedCoffee = {};
coffeeRepository.findOne.mockReturnValue(expectedCoffee);
const coffee = await service.findOne(coffeeId);
expect(coffee).toEqual(expectedCoffee);
});
});
describe('otherwise', () => {
it('should throw the "NotFoundException"', async (done) => {
const coffeeId = '1';
coffeeRepository.findOne.mockReturnValue(undefined);
try {
await service.findOne(coffeeId);
done();
} catch (err) {
expect(err).toBeInstanceOf(NotFoundException);
expect(err.message).toEqual(`Coffee #${coffeeId} not found`);
}
});
});
});
});