Controller
@Controller(API path): 裝飾器 + Class
CRUD Controller: 通過命令行自動生成
nest g resource [name]
在controller如何返回response到調(diào)用方,兩種方法。
- Standard(Recommend) : built-in method. 如果返回的是一個JS 對象或數(shù)組,會被自動序列化成Json。如果是JS 主類型, 并不會序列化,而是直接發(fā)送回去。 同時,返回的http狀態(tài)碼,總是200. 除了POST, 返回201。可以通過@HttpCode(...) 裝飾器來修改它。
- Library-specific: 也可以使用Express的response object。 通過使用@Res()裝飾器。例如: findAll(@Res() response))。這樣可直接訪問原生的response對象。比如response.status(200).send()。例如:
@Controller('cats')
export class CatsController {
@Post()
create(@Res() res: Response) {
res.status(HttpStatus.CREATED).send();
}
@Get()
findAll(@Res() res: Response) {
res.status(HttpStatus.OK). json ([]);
}
}
如何訪問request
對象
使用@Req()裝飾器。例如:
@Get()
findAll(@Req() request: Request): string {
return 'This action returns all cats';
}
也可以獨立使用的裝飾器
@Body(),@Query(),@Param,@Query,@Headers,@HostParam()
Controller自定義響應(yīng)給客戶端的Header
@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat';
}
或者使用library的方式:res.header()
在Controller中重定向請求
使用@Redirect()裝飾器, 兩個參數(shù),url, 狀態(tài)碼,默認是302.
@Get()
@Redirect('https://nestjs.com', 301)
如果想動態(tài),設(shè)置重定向地址,直接return 這樣的數(shù)據(jù)結(jié)構(gòu)。
{
"url": string,
"statusCode": number
}
路由參數(shù)
@Get(':id')
findOne(@Param() params): string {
console.log(params.id);
return This action returns a #${params.id} cat;
}
@Get(':id')
findOne(@Param('id') id: string): string {
return This action returns a #${id} cat;
}
The @Controller decorator can take a host option to require that the HTTP host of the incoming requests matches some specific value.
@Controller({ host: 'admin.example.com' })
export class AdminController {
@Get()
index(): string {
return 'Admin page';
}
}
可以通過token,獲取動態(tài)子域名
@Controller({ host: ':account.example.com' })
export class AccountController {
@Get()
getInfo(@HostParam('account') account: string) {
return account;
}
}
Controller中的異步Asynchronicity
直接使用async / promise 作為controller返回值
@Get()
async findAll(): Promise<any[]> {
return [];
}
// 同時支持Observe 返回
@Get()
findAll(): Observable<any[]> {
return of([]);
}
Controller中發(fā)送過來的RequestPayLoad, 可以使用plain class 定義DTO類,NestJS自動做轉(zhuǎn)換。但是,對比較大的數(shù)據(jù),比如Product(一個商品類),會比較麻煩。也可以直接使用any類型,NestJs會轉(zhuǎn)。
Provider
是一個Service層,邏輯代碼和后端調(diào)用可以寫在這里: 通過依賴注入,
Services: 可以通過nest命令來生成一個Service。
nest g service cats
Service使用 @Injectable() 裝飾器。告訴Nest, 這個可以被 IoC container管理。默認Service都是單例的,可以被共享,第一個調(diào)用的,會初始化后,注冊到IoC管理中,默認使用class名字作為標(biāo)志,來進行查找。
在Controller中注入Service, 使用Controller的構(gòu)造器:下面,會自動注入CatService。
constructor(private catsService: CatsService) {}
Dependency injection: 依賴注入
通過構(gòu)造器,Nest會返回一個存在的實例。 這個依賴就會注入到controller中。
- Scope
Provider正常有一個Scope生命周期。當(dāng)應(yīng)用啟動的時候,每一個依賴都要解決掉。所以,每一個provider會被初始化。當(dāng)應(yīng)用shutdown的時候,每一個provider 會被destroyed. 當(dāng)然,也可以讓你的provider擁有request-scope的生命周期。
- 自定義provider
Nest有內(nèi)建的Inversion of controller(IoC) 容器來解決provider之間的關(guān)系。有好幾種定義provider的方法: Plain Value, class, 同步/異步工廠。
- 可選provider
使用@Optional() 裝飾器, HttpService依賴的Provider, "HTTP_OPTIONS"是可選的。
@Injectable()
export class HttpService<T> {
constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}
- 基于屬性的注入
前面都是基于構(gòu)造器的注入。還可以使用基于屬性的注入。
@Injectable()
export class HttpService<T> {
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
}
- 注冊provider
provider要在本模塊內(nèi)的controller使用的話,必須在本模塊的provider里面進行注冊,才可以被controller注入。
- 手動實例化
獲取存在的實例或者動態(tài)實例化provider。 我們可以使用module reference.
- Provider類型,使用
Value providers: useValue
@Module({
imports: [CatsModule],
providers: [
{
provide: CatsService,
useValue: mockCatsService,
},
],
})
// mockCatsService 是一個plain object.
Non-class-based provider tokens
使用class名字,作為我們的provider token, provider列表的唯一標(biāo)志。(默認)
也可以使用string作為token。
@Module({
providers: [
{
provide: 'CONNECTION',
useValue: connection,
},
],
})
這個使用string 作為token., 注入的時候,使用Inject(名字),注入。
@Injectable()
export class CatsRepository {
constructor(@Inject('CONNECTION') connection: Connection) {}
}
Class providers: useClass
動態(tài)決定應(yīng)該使用哪個類實例。比如,有一個抽象類ConfigService, 根據(jù)環(huán)境來初始化。
const configServiceProvider = {
provide: ConfigService,
useClass:process.env.NODE_ENV === 'development'
? DevelopmentConfigService
: ProductionConfigService,
};
@Module({
providers: [configServiceProvider],
})
Factory providers: useFactory
動態(tài)創(chuàng)建provider.
const connectionProvider = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider, optionalProvider?: string) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider, { token: 'SomeOptionalProvider', optional: true }],
// \_____________/ \__________________/
// This provider The provider with this
// is mandatory. token can resolve to undefined.
};
@Module({
providers: [
connectionProvider,
OptionsProvider,
// { provide: 'SomeOptionalProvider', useValue: 'anything' },
],
})
Alias providers: useExisting
const loggerAliasProvider = {
provide: 'AliasedLoggerService',
useExisting: LoggerService,
};
@Module({
providers: [LoggerService, loggerAliasProvider],
})
Non-service based providers
provider 可以是任何value。
const configFactory = {
provide: 'CONFIG',
useFactory: () => {
return process.env.NODE_ENV === 'development' ? devConfig : prodConfig;
},
};
@Module({
providers: [configFactory],
})
Export custom provider: Exporting using the token
const connectionFactory = {
provide: 'CONNECTION',
useFactory: (optionsProvider: OptionsProvider) => {
const options = optionsProvider.get();
return new DatabaseConnection(options);
},
inject: [OptionsProvider],
};
@Module({
providers: [connectionFactory],
exports: ['CONNECTION'],
})
// exporting with full provider object.
@Module({
providers: [connectionFactory],
exports: [connectionFactory],
})
Asynchronous providers
如果你想數(shù)據(jù)庫連接建立好后,才能開始接受請求,可以使用異步provider。
{
provide: 'ASYNC_CONNECTION',
useFactory: async () => {
const connection = await createConnection(options);
return connection;
},
}
Modules
定義模塊使用:@Module() decorator. 裝飾器。每一個應(yīng)用至少一個模塊,叫做Root Module。
@Module()
- providers: the providers that will be instantiated by the Nest injector and that may be shared at least across this module: 本模塊使用的providers.
- controllers: the set of controllers defined in this module which have to be instantiated. controller集合,需要初始化的。
- imports: the list of imported modules that export the providers which are required in this module. 倒入的模塊,這樣在本模塊中可以使用該倒入模塊的導(dǎo)出providers.
- exports: the subset of providers that are provided by this module and should be available in other modules which import this module. 導(dǎo)出的provider集合,可以被其他模塊導(dǎo)入后注入。
生成一個模塊使用nest CLI: nest g module cats
modules默認是單例的。
- Module re-exporting
@Module({
imports: [CommonModule],
exports: [CommonModule],
})
export class CoreModule {}
這樣,導(dǎo)入CoreModule的,就自動就導(dǎo)入了CommonModule.
- Dependency injection
一個模塊,也可以注入provider來使用。
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {
constructor(private catsService: CatsService) {}
}
模塊也能注入providers. 但是模塊不能被注入到providers.
- Global modules
Nest模塊缺省不是global注冊的,所以,你在某個模塊中使用的話,比如先導(dǎo)入該模塊。
如果想全局注冊模塊,使用global with the @Global() decorator.
@Global()
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
- Dynamic modules
能夠讓你動態(tài)創(chuàng)建,注冊模塊。
@Module({
providers: [Connection],
})
export class DatabaseModule {
static forRoot(entities = [], options?): DynamicModule {
const providers = createDatabaseProviders(options, entities);
return {
module: DatabaseModule,
providers: providers,
exports: providers,
};
}
}
// 使用
@Module({
imports: [DatabaseModule.forRoot([User])],
})
export class AppModule {}
// re-export
@Module({
imports: [DatabaseModule.forRoot([User])],
exports: [DatabaseModule],
})
export class AppModule {}
MIDDLEWARE
中間件,是在router之前被調(diào)用。Client Side -> Middleware -> RouteHandler.
可以執(zhí)行:
執(zhí)行任何代碼
修改request, response 對象。
結(jié)束Request-Response
call 下一個中間件
如果當(dāng)前中間件,不結(jié)束request-response cycle。 必須調(diào)用next()。 否則請求會被掛起。
實現(xiàn)中間件:
可以使用function 或者class(@Injectable)來實現(xiàn)一個中間件。類必須實現(xiàn)NestMiddleware接口。function沒有任何特殊要求。
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
}
- Dependency injection
完全支持依賴,注入系統(tǒng),通常通過構(gòu)造器。
使用 middleware
通常,我們使用configure() method,來初始化,配置中間件。下面我們初始化日志中間件。
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware)
.forRoutes('cats');
}
}
我們也可以限制中間件到特定的path, request methods. Eg: .forRoutes({ path: 'cats', method: RequestMethod.GET });
- Route wildcards
可以使用通配符。.forRoutes({ path: 'ab*cd', method: RequestMethod.ALL });
- Middleware consumer
使用鏈?zhǔn)秸{(diào)用, The apply() method may either take a single middleware, or multiple arguments to specify multiple middlewares.
- Excluding routes
consumer.apply(LoggerMiddleware)
.exclude(
{ path: 'cats', method: RequestMethod.GET },
{ path: 'cats', method: RequestMethod.POST },
'cats/(.*)',)
.forRoutes(CatsController);
- Functional middleware
export function logger(req: Request, res: Response, next: NextFunction) {
console.log(Request...);
next();
};
consumer.apply(logger)
.forRoutes(CatsController);
- Multiple middleware
consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);
- Global middleware
const app = await NestFactory.create(AppModule);
app.use(logger);
await app.listen(3000);
Exception Filter
內(nèi)建的Exception filter, 處理異常,返回給客戶端。
有一個全局的內(nèi)建異常過濾器,來處理HttpException以及它子類。
如果異常不能識別,全局過濾器,會生成下面的json字符。
{
"statusCode": 500,
"message": "Internal server error"
}
- 拋出標(biāo)準(zhǔn)異常
@Get()
async findAll() {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
- 自定義異常
需要繼承HttpException
- 異常過濾器:Exception filters
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
response.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
- Binding 過濾器
過濾器可以綁定到controller的方法上,類名上,也可以全局注冊。
@Post()
@UseFilters(HttpExceptionFilter)
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
@UseFilters(new HttpExceptionFilter())
export class CatsController {}
app.useGlobalFilters(new HttpExceptionFilter());
//全局注冊,是不和任意模塊綁定的。我們也可以在某個模塊中注冊全局filters。
@Module({
providers: [
{
provide: APP_FILTER,
useClass: HttpExceptionFilter,
},
],
})
- Catch everything
@Catch()
Pipe
主要的應(yīng)用場景:
- transformation: 轉(zhuǎn)換輸入數(shù)據(jù)到需要的格式,比如從String 到Integer.
- validation: 校驗輸入的數(shù)據(jù)格式,如果不合格,拋出異常。
- 綁定 pipe
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
- 自定義 pipe
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
return value;
}
}
- 基于Schema的校驗
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
對象的schema校驗,有兩種方法:
The Joi library allows you to create schemas in a straightforward way, with a readable API. Let's build a validation pipe that makes use of Joi-based schemas.
npm install --save joi
import { ObjectSchema } from 'joi';
@Injectable()
export class JoiValidationPipe implements PipeTransform {
constructor(private schema: ObjectSchema) {}
transform(value: any, metadata: ArgumentMetadata) {
const { error } = this.schema.validate(value);
if (error) {
throw new BadRequestException('Validation failed');
}
return value;
}
}
const createCatSchema = Joi.object({
name: Joi.string().required(),
age: Joi.number().required(),
breed: Joi.string().required(),
})
export interface CreateCatDto {
name: string;
age: number;
breed: string;
}
@Post()
@UsePipes(new JoiValidationPipe(createCatSchema))
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
Class validator
npm i --save class-validator class-transformer
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString()
name: string;
@IsInt()
age: number;
@IsString()
breed: string;
}
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value;
}
const object = plainToInstance(metatype, value);
const errors = await validate(object);
if (errors.length > 0) {
throw new BadRequestException('Validation failed');
}
return value;
}
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object];
return !types.includes(metatype);
}
}
}
// Controller
@Post()
async create(@Body(new ValidationPipe()) createCatDto: CreateCatDto,) {
this.catsService.create(createCatDto);
}
全局pipes
app.useGlobalPipes(new ValidationPipe());
- 模塊級的全局注冊
@Module({
providers: [
{
provide: APP_PIPE,
useClass: ValidationPipe,
},
],
})
Transformation use case: 轉(zhuǎn)換的使用場景
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const val = parseInt(value, 10);
if (isNaN(val)) {
throw new BadRequestException('Validation failed');
}
return val;
}
}
//Controller
@Get(':id')
async findOne(@Param('id', new ParseIntPipe()) id) {
return this.catsService.findOne(id);
}
提供缺省值
Parse* pipes 期待參數(shù)是必須提供的。 這樣,某些情況下,我們需要提供缺省值,使用DefaultValuePipe。
@Get()
async findAll(
@Query('activeOnly', new DefaultValuePipe(false), ParseBoolPipe) activeOnly: boolean,
@Query('page', new DefaultValuePipe(0), ParseIntPipe) page: number,
) {
return this.catsService.findAll({ activeOnly, page });
}
GUARDS
Guards 有單一職責(zé): Guards決定是否當(dāng)前的請求,能否被controller處理。可以根據(jù)權(quán)限許可,Roles, ACL等。
以前是被Middleware處理,但是middleware不了解具體的controller。 而guards可以訪問到ExecutionContext instance。這樣就能知道下一個執(zhí)行。
- Authorization guard,認證守衛(wèi)。
@Injectable()
export class AuthGuard implements CanActivate {
canActivate( context: ExecutionContext,): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
return validateRequest(request);
}
}
// if it returns true, the request will be processed.
// if it returns false, Nest will deny the request.
- Execution context, 執(zhí)行上下文
The ExecutionContext inherits from ArgumentsHost
- 基于角色的認證
Setting roles per handler
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
最好是創(chuàng)建自己的裝飾器,而不是直接使用setMetadata。例如:
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
@Post()
@Roles('admin')
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
return matchRoles(roles, user.roles);
}
}
Interceptor
主要用于:AOP編程。
- 在controller執(zhí)行前,做額外的邏輯處理
- 轉(zhuǎn)換結(jié)果
- 轉(zhuǎn)換異常
- 繼承基本邏輯功能
- 覆蓋特定邏輯
攔截器中,第一個參數(shù)是Execution context, 第二個是call hander.
在調(diào)用next.handle()之前的代碼,是攔截器開始執(zhí)行,handle()后的代碼是在返回response的時候攔截器執(zhí)行。(洋蔥模型)
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();
return next.handle().pipe(
tap(() => console.log(After... ${Date.now() - now}ms)),
);
}
}
- 綁定攔截器
@UseInterceptors(LoggingInterceptor)
export class CatsController {}
定義全局攔截器
app.useGlobalInterceptors(new LoggingInterceptor());
定義模塊級的全局攔截器
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
],
})
響應(yīng)的映射:Response mapping
@Injectable()
export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> {
intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
return next.handle().pipe(map(data => ({ data })));
}
}
假如,我們需要null value轉(zhuǎn)成 empty string ''
@Injectable()
export class ExcludeNullInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(map(value => value === null ? '' : value ));
}
}
- Exception mapping
我們可以使用RxJS的catchError,去覆蓋異常。
@Injectable()
export class ErrorsInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle()
.pipe(
catchError(err => throwError(() => new BadGatewayException())),
);
}
}
- 處理流覆蓋:Stream overriding
比如:使用cache interceptor,* 如果緩存了,路由****controller****,不會被執(zhí)行到。*
@Injectable()
export class CacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const isCached = true;
if (isCached) {
return of([]);
}
return next.handle();
}
}
Custom route decorators
參數(shù)裝飾器, 可以和route handle一起使用。
// 沒有data
export const User = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
// Controller
@Get()
async findOne(@User() user: UserEntity) {
console.log(user);
}
//有data
export const User = createParamDecorator(
(data: string, ctx: ExecutionContext) => {
const request = ctx.switchToHttp().getRequest();
const user = request.user;
return data ? user?.[data] : user;
},
)
// controller中,裝飾器,可以傳data
@Get()
async findOne(@User('firstName') firstName: string) {
console.log(Hello ${firstName});
}
和管道一起使用
@Get()
async findOne(
@User(new ValidationPipe({ validateCustomDecorators: true }))
user: UserEntity,) {
console.log(user);
}
- 組合裝飾器: Decorator composition
export function Auth(...roles: Role[]) {
return applyDecorators(
SetMetadata('roles', roles),
UseGuards(AuthGuard, RolesGuard),
ApiBearerAuth(),
ApiUnauthorizedResponse({ description: 'Unauthorized' }),
);
}
// Controller中
@Get('users')
@Auth('admin')
findAllUsers() {}
NestJS 生命周期
- Bootstrap
- onModuleInit。// 在依賴處理的時候調(diào)用。調(diào)用順序依賴模塊導(dǎo)入順序。
- onApplicationBootstrap //所有模塊都初始化好了后調(diào)用。
- start listener
- application is running
- onModuleDestroy //在收到shutdown信號后調(diào)用。
- beforeApplicationShutdown //
- stop listener
- onApplicationShutdown
- process Exit
** Nest 在應(yīng)用啟動時調(diào)用,并且調(diào)用所有modules, injectables, 和controllers上的鉤子。
可以通過在controller, provider, module上實現(xiàn)onModuleInit方法,來完成。
- Asynchronous initialization
onModuleInit, onApplicationBootstrap的鉤子允許defer 初始化過程。
async onModuleInit(): Promise<void> {
await this.fetch();
}
Client端請求生命周期
In general, the request lifecycle looks like the following:
- Incoming request
- Globally bound middleware
- Module bound middleware
- Global guards
- Controller guards
- Route guards
- Global interceptors (pre-controller)
- Controller interceptors (pre-controller)
- Route interceptors (pre-controller)
- Global pipes
- Controller pipes
- Route pipes
- Route parameter pipes
- Controller (method handler)
- Service (if exists)
- Route interceptor (post-request)
- Controller interceptor (post-request)
- Global interceptor (post-request)
- Exception filters (route, then controller, then global)
- Server response