Controller
@Controller(API path): 装饰器 + Class
CRUD Controller: 通过命令行自动生成
nest g resource [name]
在controller如何返回response到调用方,两种方法。
- Standard(Recommend) : built-in method. 如果返回的是一个JS 对象或数组,会被自动序列化成Json。如果是JS 主类型, 并不会序列化,而是直接发送回去。 同时,返回的http状态码,总是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自定义响应给客户端的Header
@Post()
@Header('Cache-Control', 'none')
create() {
return 'This action adds a new cat';
}
或者使用library的方式:res.header()
在Controller中重定向请求
使用@Redirect()装饰器, 两个参数,url, 状态码,默认是302.
@Get()
@Redirect('https://nestjs.com', 301)
如果想动态,设置重定向地址,直接return 这样的数据结构。
{
"url": string,
"statusCode": number
}
路由参数
@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,获取动态子域名
@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中发送过来的RequestPayLoad, 可以使用plain class 定义DTO类,NestJS自动做转换。但是,对比较大的数据,比如Product(一个商品类),会比较麻烦。也可以直接使用any类型,NestJs会转。
Provider
是一个Service层,逻辑代码和后端调用可以写在这里: 通过依赖注入,
Services: 可以通过nest命令来生成一个Service。
nest g service cats
Service使用 @Injectable() 装饰器。告诉Nest, 这个可以被 IoC container管理。默认Service都是单例的,可以被共享,第一个调用的,会初始化后,注册到IoC管理中,默认使用class名字作为标志,来进行查找。
在Controller中注入Service, 使用Controller的构造器:下面,会自动注入CatService。
constructor(private catsService: CatsService) {}
Dependency injection: 依赖注入
通过构造器,Nest会返回一个存在的实例。 这个依赖就会注入到controller中。
- Scope
Provider正常有一个Scope生命周期。当应用启动的时候,每一个依赖都要解决掉。所以,每一个provider会被初始化。当应用shutdown的时候,每一个provider 会被destroyed. 当然,也可以让你的provider拥有request-scope的生命周期。
- 自定义provider
Nest有内建的Inversion of controller(IoC) 容器来解决provider之间的关系。有好几种定义provider的方法: Plain Value, class, 同步/异步工厂。
- 可选provider
使用@Optional() 装饰器, HttpService依赖的Provider, "HTTP_OPTIONS"是可选的。
@Injectable()
export class HttpService<T> {
constructor(@Optional() @Inject('HTTP_OPTIONS') private httpClient: T) {}
}
- 基于属性的注入
前面都是基于构造器的注入。还可以使用基于属性的注入。
@Injectable()
export class HttpService<T> {
@Inject('HTTP_OPTIONS')
private readonly httpClient: T;
}
- 注册provider
provider要在本模块内的controller使用的话,必须在本模块的provider里面进行注册,才可以被controller注入。
- 手动实例化
获取存在的实例或者动态实例化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列表的唯一标志。(默认)
也可以使用string作为token。
@Module({
providers: [
{
provide: 'CONNECTION',
useValue: connection,
},
],
})
这个使用string 作为token., 注入的时候,使用Inject(名字),注入。
@Injectable()
export class CatsRepository {
constructor(@Inject('CONNECTION') connection: Connection) {}
}
Class providers: useClass
动态决定应该使用哪个类实例。比如,有一个抽象类ConfigService, 根据环境来初始化。
const configServiceProvider = {
provide: ConfigService,
useClass:process.env.NODE_ENV === 'development'
? DevelopmentConfigService
: ProductionConfigService,
};
@Module({
providers: [configServiceProvider],
})
Factory providers: useFactory
动态创建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
如果你想数据库连接建立好后,才能开始接受请求,可以使用异步provider。
{
provide: 'ASYNC_CONNECTION',
useFactory: async () => {
const connection = await createConnection(options);
return connection;
},
}
Modules
定义模块使用:@Module() decorator. 装饰器。每一个应用至少一个模块,叫做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. 倒入的模块,这样在本模块中可以使用该倒入模块的导出providers.
- exports: the subset of providers that are provided by this module and should be available in other modules which import this module. 导出的provider集合,可以被其他模块导入后注入。
生成一个模块使用nest CLI: nest g module cats
modules默认是单例的。
- Module re-exporting
@Module({
imports: [CommonModule],
exports: [CommonModule],
})
export class CoreModule {}
这样,导入CoreModule的,就自动就导入了CommonModule.
- Dependency injection
一个模块,也可以注入provider来使用。
@Module({
controllers: [CatsController],
providers: [CatsService],
})
export class CatsModule {
constructor(private catsService: CatsService) {}
}
模块也能注入providers. 但是模块不能被注入到providers.
- Global modules
Nest模块缺省不是global注册的,所以,你在某个模块中使用的话,比如先导入该模块。
如果想全局注册模块,使用global with the @Global() decorator.
@Global()
@Module({
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
- Dynamic modules
能够让你动态创建,注册模块。
@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之前被调用。Client Side -> Middleware -> RouteHandler.
可以执行:
执行任何代码
修改request, response 对象。
结束Request-Response
call 下一个中间件
如果当前中间件,不结束request-response cycle。 必须调用next()。 否则请求会被挂起。
实现中间件:
可以使用function 或者class(@Injectable)来实现一个中间件。类必须实现NestMiddleware接口。function没有任何特殊要求。
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
}
- Dependency injection
完全支持依赖,注入系统,通常通过构造器。
使用 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
使用链式调用, 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
内建的Exception filter, 处理异常,返回给客户端。
有一个全局的内建异常过滤器,来处理HttpException以及它子类。
如果异常不能识别,全局过滤器,会生成下面的json字符。
{
"statusCode": 500,
"message": "Internal server error"
}
- 抛出标准异常
@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
主要的应用场景:
- transformation: 转换输入数据到需要的格式,比如从String 到Integer.
- validation: 校验输入的数据格式,如果不合格,抛出异常。
- 绑定 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: 转换的使用场景
@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 期待参数是必须提供的。 这样,某些情况下,我们需要提供缺省值,使用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 有单一职责: Guards决定是否当前的请求,能否被controller处理。可以根据权限许可,Roles, ACL等。
以前是被Middleware处理,但是middleware不了解具体的controller。 而guards可以访问到ExecutionContext instance。这样就能知道下一个执行。
- Authorization guard,认证守卫。
@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, 执行上下文
The ExecutionContext inherits from ArgumentsHost
- 基于角色的认证
Setting roles per handler
@Post()
@SetMetadata('roles', ['admin'])
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
最好是创建自己的装饰器,而不是直接使用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执行前,做额外的逻辑处理
- 转换结果
- 转换异常
- 继承基本逻辑功能
- 覆盖特定逻辑
拦截器中,第一个参数是Execution context, 第二个是call hander.
在调用next.handle()之前的代码,是拦截器开始执行,handle()后的代码是在返回response的时候拦截器执行。(洋葱模型)
@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,
},
],
})
响应的映射: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转成 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****,不会被执行到。*
@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
参数装饰器, 可以和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。// 在依赖处理的时候调用。调用顺序依赖模块导入顺序。
- onApplicationBootstrap //所有模块都初始化好了后调用。
- start listener
- application is running
- onModuleDestroy //在收到shutdown信号后调用。
- beforeApplicationShutdown //
- stop listener
- onApplicationShutdown
- process Exit
** Nest 在应用启动时调用,并且调用所有modules, injectables, 和controllers上的钩子。
可以通过在controller, provider, module上实现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