본문은 Nest.js의 공식문서를 요약한 것으로 Nest.js를 대략적으로 파악하는 것에 초점을 두고 현재 단계에서 너무 디테일하다고 판단되는 부분들은 스킵하였습니다.
src
ㄴ app.controller.spec.ts
ㄴ app.controller.ts
ㄴ app.module.ts
ㄴ app.service.ts
ㄴ main.ts
Nest aims to be a platform-agnostic(platform과 무관한) framework. express and fastify are supported out-of-the-box.
Controllers are responsible for handling incoming requests and returing responses to the client.
The routing mechanism controls which controller receives which requests.
In Nest.js, we use classes and decorators. Decorators associate classes with required metadata and enable Nest to create a routing map.
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll(): string {
return 'This action returns all cats';
}
}
@Controller('cats')
를 통해 endpoint를 지정하고@Get()
을 통해 HTTP method를 지정한다.@Controller('customers')
export class CustomersController {
@Get('profile')
getProfile(): string {
return 'customer profile!'
}
}
GET customers/profile
에 바인드 된다.위의 두 예시에서 볼 수 있듯이 endpoint에 바인드되는 method의 이름은 아무런 의미가 없다.
request handler는 default로 JS primitive의 경우 그대로 반환하고, JS object나 array의 경우 JSON-serialize한 뒤 return 한다. 설정을 변경하면 Library-specific(e.g., Express)한 response object를 반환할 수 있다.
@Get(':id')
findOne(@Param() params): string {
console.log(params.id);
return `This action returns a #${params.id} cat`;
}
위와 같이 하면 cats/1
과 같은 request를 처리할 수 있다.
POST
의 body
로 넘어오는 DTO(Data Transfer Object) Schema를 선언할 때 Typescript의 interface를 사용하여도 되고, ES6의 class를 사용하여도 된다. 하지만 Typescript는 transpile되어 실행될 때 사라지기 때문에 Nest가 runtime때 참고할 수 없기 때문에 class형식으로 선언하는 것을 더 추천한다.
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
//controller
@Post()
async create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
Providers are a fundamental concept in Nest. The main idea of a provider is that it can be injected as dependency; this means objects can create various relationships with each other. Providers are plain JS classes that are declared as providers
in a module.
Dependency injection is basically providing the objects that an object needs (its dependencies) instead of having it construct them itself. It's a very useful technique for testing, since it allows dependencies to be mocked or stubbed out.
This service will be responsible for data storage and retrieval, and is designed to be used by CatsController
.
@Injectable()
export class CatsService {
private readonly cats: Cat[] = [];
create(cat: Cat) {
this.cats.push(cat);
}
findAll(): Cat[] {
return this.cats;
}
}
The only new feature is that it uses the @Injectable()
decorator. The @Injectable()
decorator attaches metadata, which declares that CatsService
is a class that can be managed by the Nest IoC container.
IoC stands for Inversion of Control
@Controller('cats')
export class CatsController {
// 위어서 선언한 @Injectable CatsService를 declar & initialize
// Dependency Injection
constructor(private catsService: CatsService) {}
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}
A module is a class annotated with @Module()
decorator. Decorator provides metadata that Nest makes use of to organize the application structure.
Each application has at least one module, a root module. The root module is the starting point Nest uses to build the application graph - the internal data structure nest uses to resolve module and provider relationships and dependencies.
@Module
decorator는 하나의 object를 받으며 해당 object는 아래의 property들을 가질 수 있다
provider
controllers
imports
exports
The module encapsulates providers by default. This means that it's impossible to inject providers that are neighter directly part of the current module nor exported from the imported modules. Thus, you may consider the exported providers from a module as the module's public interface, or API.
In Nest, modules are singletons by default, and thus you can share the same instance of any provider betwween multiple modules effortlessly.
Also, Every module is automatically a shared module. Once created it can be reused by any module.
// cats/cats.module.ts
// CAT MODULE
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {}
// app.module.ts
// ROOT MODULE
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule {}
Middleware is a function which is called before the route handler. Middleware functions have access to the request and response objects.
You implement custom Nest middleware in either a function, or in a class with an @Injectable()
decorator.
The class should implement the NestMiddleware
interface, while the function does not have any special requirements.
// logger.middleware.ts
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
}
*Nest middleware fully supports DI
// app.module.ts
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('cats');
//.forRoutes({ path: 'cats', method: RequestMethod.GET });
//.forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });
}
}
Excluding routes
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'cats', method: RequestMethod.GET },
{ path: 'cats', method: RequestMethod.POST },
'cats/(.*)',
)
.forRoutes(CatsController);
// logger.middleware.ts
export function logger(req: Request, res: Response, next: NextFunction) {
console.log(`Request...`);
next();
};
// app.module.ts
consumer
.apply(logger)
.forRoutes(CatsController);
consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);
Applied 3 middlewares, cors
, helmet
, logger
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
Nest comes with a built-in exceptions layer which is responsible for processing all unhandled exceptions across an application. When an exception is not handled by your application code, it is caught by this layer.
Exception filters are designed for cases where you may want full control over the exception layer. They let you control the exact flow of control and the content of the response sent back to the client.
A pipe is a class annotated with the @Injectable()
decorator. Pipes should implement PipeTransform
interface.
Pipes have two typical use cases:
In both cases, pipes operate on the arugments
being processed by a controller route handler. Nest interposes a pipe just before a method is invoked, and the pipe receives the arguments destined for the method and operates on them.
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
parameter로 넘어오는 id가 integer
타입인지 확인하고 아닐경우 400 Bad Request
를 발생시킨다.
Every pipe must implement the transform()
method to filtull the PipeTransform
interface contract. This method has two parameters, value
and metadata
.
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
A guard is a class annotated with the @Injectable()
decorator. Guards should implement the CanActivate
interface.
Guards have a single responsibility. They determine whether a given request will be handled by the route handler or not, depending on certain conditions (like permissions, roles, ACLs, etc.) present at run-time. This is often referred to as authorization. Authorization (and its cousin, authentication, with which it usually collaborates) has typically been handled by middleware in traditional Express applications.