源码:awsome-nestnode
在 nestjs入门(一) 中,对 Nestjs 一些重要的概念有了一些了解,如今咱们开始建立一个基于 Nestjs 的应用吧。mysql
Nestjs 和 Angular 同样,提供了 CLI 工具帮助咱们初始化和开发应用程序。git
$ npm install -g @nestjs/cli
$ nest new my-awesome-app
复制代码
这时候你会获得这样的一个目录结构:github
运行npm start
后,在浏览器访问http://localhost:3000/
就能够看到Hello World!
。sql
在 Nestjs 中,全部的 controller 和 service 都要在对应的 module 中注册,就像这样:typescript
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
复制代码
在 MVC 模式中,controller 经过 model 获取数据。对应的,在 Nestjs 中,controller 负责处理传入的请求, 并调用对应的 service 完成业务处理,返回对客户端的响应。数据库
一般能够经过 CLI 命令来建立一个 controller:npm
$ nest g co cats
复制代码
这时候,CLI 会自动生成 controller 文件,而且把 controller 注册到对应的 module 中。json
和其余一些 node 框架不同,Nestjs 路由不是集中式管理,而是分散在 controller 中,经过@controller()
中声明的(可选)前缀和请求装饰器中指定的任何路由来肯定的。bootstrap
import { Controller, Get } from '@nestjs/common';
import { CatsService } from './cats.service';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {
}
@Get(':id')
findOne(@Param('id') id: string): string {
return this.catsService.getCat();
}
}
复制代码
上面这段代码中,经过 Get 请求去请求http://localhost:3000/cats/1
就会调用findOne
方法。
若是须要在全部请求以前加上 prefix,能够在main.ts
中直接设置 GlobalPrefix:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api/v1');
await app.listen(3000);
}
bootstrap();
复制代码
在 Nestjs 中,controller 就像是调用 service 的指挥者,把对应的请求分发到相应的 service 中去处理。
在 controller 中,咱们注意到,在构造函数中注入了CatsService
实例,来调用对应 service 中的方法。这就是 Nestjs 中依赖注入的注入方式 — 构造函数注入。
service 能够看作夹在 controller 和 model 之间的一层,在 service 调用 DAO (在 Nestjs 中是各类 ORM 工具或者本身封装的 DAO 层)实现数据库的访问,进行数据的处理整合。
import { Injectable } from '@nestjs/common';
@Injectable()
export class CatsService {
getCat(id: string): string {
return `This action returns ${id} cats`;
}
}
复制代码
上面代码中经过@Injectable()
定义了一个 service,这样你就能够在其余 controller 或者 service 中注入这个 service。
经过nestjs入门(一)已经介绍了 DTO 的概念,在Nestjs 中,DTO 主要定义如何经过网络发送数据的对象,一般会配合class-validator
和class-transformer
作校验。
import { IsString, IsInt } from 'class-validator';
export class CreateCatDto {
@IsString()
readonly name: string;
@IsInt()
readonly age: number;
@IsString()
readonly breed: string;
}
复制代码
import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto } from './dto';
@Controller('cats')
export class CatsController {
@Post()
create(@Body() createCatDto: CreateCatDto) {
return 'This action adds a new cat';
}
}
复制代码
上面对请求body
定义了一个 DTO,而且在 DTO 中对参数类型进行了限制,若是body
中传过来的类型不符合要求,会直接报错。
DTO 中的class-validator
还须要配合 pipe 才能完成校验功能:
import {
PipeTransform,
ArgumentMetadata,
BadRequestException,
Injectable,
} from '@nestjs/common'
import { validate } from 'class-validator'
import { plainToClass } from 'class-transformer'
import * as _ from 'lodash'
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value, metadata: ArgumentMetadata) {
const { metatype } = metadata
if (!metatype || !this.toValidate(metatype)) {
return value
}
const object = plainToClass(metatype, value)
const errors = await validate(object)
if (errors.length > 0) {
const errorMessage = _.values(errors[0].constraints)[0]
throw new BadRequestException(errorMessage)
}
return value
}
private toValidate(metatype): boolean {
const types = [String, Boolean, Number, Array, Object]
return !types.find(type => metatype === type)
}
}
复制代码
这个 pipe 会根据元数据和对象实例,去构建原有类型,而后经过validate
去校验。
这个 pipe 通常会做为全局的 pipe 去使用:
async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
app.setGlobalPrefix('api/v1');
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();
复制代码
假设咱们没有这层 pipe,那在 controller 中就会进行参数校验,这样就会打破单一职责的原则。有了这一层 pipe 帮助咱们校验参数,有效地下降了类的复杂度,提升了可读性和可维护性。
代码写到这里,咱们发现直接返回了字符串,这样有点太粗暴,须要把正确和错误的响应包装一下。假设我但愿返回的格式是这样的:
# 请求成功
{
status: 0,
message: '请求成功',
data: any
}
# 请求失败
{
status: 1,
message: string,
}
复制代码
此时,能够利用 AOP 的思想去作这件事。首先,咱们须要全局捕获错误的切片层去处理全部的 exception;其次,若是是一个成功的请求,须要把这个返回结果经过一个切片层包装一下。
在 Nestjs 中,返回请求结果时,Interceptor 会在 Exception Filter 以前触发,因此 Exception Filter 会是最后捕获 exception的机会。咱们把它做为处理全局错误的切片层。
import {
Catch,
ArgumentsHost,
HttpException,
ExceptionFilter,
HttpStatus,
} from '@nestjs/common'
@Catch()
export class ExceptionsFilter implements ExceptionFilter {
async catch(exception, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const response = ctx.getResponse()
const request = ctx.getRequest()
let message = exception.message
let isDeepestMessage = false
while (!isDeepestMessage) {
isDeepestMessage = !message.message
message = isDeepestMessage ? message : message.message
}
const errorResponse = {
message: message || '请求失败',
status: 1,
}
const status = exception instanceof HttpException ?
exception.getStatus() :
HttpStatus.INTERNAL_SERVER_ERROR
response.status(status)
response.header('Content-Type', 'application/json; charset=utf-8')
response.send(errorResponse)
}
}
复制代码
而 Interceptor 则负责对成功请求结果进行包装:
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common'
import { Observable } from 'rxjs'
import { map } from 'rxjs/operators'
interface Response<T> {
data: T
}
@Injectable()
export class TransformInterceptor<T>
implements NestInterceptor<T, Response<T>> {
intercept(
context: ExecutionContext,
next: CallHandler,
): Observable<Response<T>> {
return next.handle().pipe(
map(rawData => {
return {
data: rawData,
status: 0,
message: '请求成功',
}
}
)
)
}
}
复制代码
一样 Interceptor 和 Exception Filter 须要把它定义在全局范围内:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api/v1');
app.useGlobalFilters(new ExceptionsFilter());
app.useGlobalInterceptors(new TransformInterceptor());
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
复制代码
TypeORM 至关于 Nestjs 中的 DAO 层,它支持多种数据库,如 PostgreSQL,SQLite 甚至MongoDB(NoSQL)。这里咱们以 MySQL 为例,首先在 MySQL 中手动建立一个数据库:
> CREATE DATABASE test
复制代码
而后安装 typeorm:
$ npm install --save @nestjs/typeorm typeorm mysql
复制代码
一般咱们开发的时候,会有多套环境,这些环境中会有不一样的数据库配置,因此先建一个config
文件夹,放置不一样的数据库配置:
// index.ts
import * as _ from 'lodash'
import { resolve } from 'path'
import productionConfig from './prod.config'
const isProd = process.env.NODE_ENV === 'production'
let config = {
port: 3000,
hostName: 'localhost',
orm: {
type: 'mysql',
host: 'localhost',
port: 3310,
username: 'root',
password: '123456',
database: 'test',
entities: [resolve(`./**/*.entity.ts`)],
migrations: ['migration/*.ts'],
timezone: 'UTC',
charset: 'utf8mb4',
multipleStatements: true,
dropSchema: false,
synchronize: true,
logging: true,
},
}
if (isProd) {
config = _.merge(config, productionConfig)
}
export { config }
export default config
复制代码
// prod.config.ts
import { resolve } from 'path'
export default {
port: 3210,
orm: {
type: 'mysql',
host: 'localhost',
port: 3312,
username: 'root',
password: '123456',
database: 'test',
entities: [resolve('./**/*.entity.js')],
migrations: ['migration/*.ts'],
dropSchema: false,
synchronize: false,
logging: false,
},
}
复制代码
在线上环境强烈不建议开启 orm 的 synchronize
功能。本地若是要开启,要注意一点,若是 entity 中定义的字段类型和数据库原有类型不同,在开启synchronize
后 orm 会执行 drop
而后再add
的操做,这会致使本地测试的时候数据丢失(这里为了方便,本地测试就把synchronize
功能打开,这样写完 entity 就会自动同步到数据库)。
在app.module.ts
中导入TypeOrmModule
:
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { CatsController } from './cats/cats.controller'
import { CatsService } from './cats/cats.service'
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'
import config from './config'
@Module({
imports: [
TypeOrmModule.forRoot(config.orm as TypeOrmModuleOptions),
],
controllers: [AppController, CatsController],
providers: [AppService, CatsService],
})
export class AppModule {}
复制代码
接下来就是写 entity,下面咱们定义了一个叫cat
的表,id
为自增主键:
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
@Entity('cat')
export class CatEntity {
@PrimaryGeneratedColumn()
id: number
@Column({ length: 50 })
name: string
@Column()
age: number
@Column({ length: 100, nullable: true })
breed: string
}
复制代码
这时候,entity 就会同步到数据库,在test
数据库中,就能看到cat
这张表了。
在某个模块使用这个 entity 的时候,须要在对应的模块中注册,使用 forFeature()
方法定义定义哪些存储库应在当前范围内注册:
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { CatsController } from './cats/cats.controller'
import { CatsService } from './cats/cats.service'
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm'
import config from './config'
import { CatEntity } from './cats/cat.entity'
const ENTITIES = [
CatEntity,
]
@Module({
imports: [
TypeOrmModule.forRoot(config.orm as TypeOrmModuleOptions),
TypeOrmModule.forFeature([...ENTITIES]),
],
controllers: [AppController, CatsController],
providers: [AppService, CatsService],
})
export class AppModule {}
复制代码
这时候就能够用@InjectRepository()
修饰器向 CatService
注入 CatRepository
:
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { CatEntity } from './cat.entity'
import { Repository } from 'typeorm'
@Injectable()
export class CatsService {
constructor(
@InjectRepository(CatEntity)
private readonly catRepository: Repository<CatEntity>,
) {
}
async getCat(id: number): Promise<CatEntity[]> {
return await this.catRepository.find({ id })
}
}
复制代码
这时候去请求http://localhost:3000/api/v1/cats/1
这个 API,就会返回下面结果:
{
"data": [],
"status": 0,
"message": "请求成功"
}
复制代码
在 typeorm 中,若是须要用到比较复杂的 sql 语句,可使用 createQueryBuilder
帮助你构建:
this.catRepository
.createQueryBuilder('cat')
.Where('name != ""')
.andWhere('age > 2')
.getMany()
复制代码
若是 createQueryBuilder
不能知足你的要求,能够直接使用query
写 sql 语句:
this.catRepository.query(
'select * from cat where name != ? and age > ?',
[age],
)
复制代码
在持续交付项目中,项目会不断迭代上线,这时候就会出现数据库改动的问题,对一个投入使用的系统,一般会使用 migration 帮咱们同步数据库。TypeORM 也自带了一个 CLI 工具帮助咱们进行数据库的同步。
首先在本地建立一个ormconfig.json
文件:
{
"type": "mysql",
"host": "localhost",
"port": 3310,
"username": "root",
"password": "123456",
"database": "test",
"entities": ["./**/*.entity.ts"],
"migrations": ["migrations/*.ts"],
"cli": {
"migrationsDir": "migrations"
},
"timezone": "UTC",
"charset": "utf8mb4",
"multipleStatements": true,
"dropSchema": false,
"synchronize": false,
"logging": true
}
复制代码
这个 json 文件中指定了 entity 和 migration 文件的匹配规则,而且在 CLI 中配置了 migration 文件放置的位置。
这时候运行下面命令就会在 migrations 文件夹下面自动生成1563725408398-update-cat.ts
文件
$ ts-node node_modules/.bin/typeorm migration:create -n update-cat
复制代码
文件名中1563725408398
是生成文件的时间戳。这个文件中会有up
和down
这两个方法:
import {MigrationInterface, QueryRunner} from "typeorm";
export class updateCat1563725408398 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
}
public async down(queryRunner: QueryRunner): Promise<any> {
}
}
复制代码
up
必须包含执行 migration 所需的代码。 down
必须恢复任何up
改变。在up
和down
里面有一个QueryRunner
对象。 使用此对象执行全部数据库操做。好比咱们在 cat 这张表中写入一个假数据:
import {MigrationInterface, QueryRunner} from "typeorm";
export class updateCat1563725408398 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`insert into cat (id, name, age, breed) values (2, 'test', 3, 'cat') `)
}
public async down(queryRunner: QueryRunner): Promise<any> {
}
}
复制代码
这时候,在 package.json
中写入下面 script 并运行npm run migration:run
,这时候 cat 表里面就会有一个id
为2
的假数据。
{
"scripts": {
"migration:run": "ts-node node_modules/.bin/typeorm migration:run",
}
}
复制代码
注意,这个ormconfig.json
文件的配置是本地环境的配置,若是须要在生成环境使用,能够从新写一份ormconfig-prod.json
,而后运行migration
命名的时候加上--config ormconfig-prod.json
。
用 typeorm 生成的 migration 有一个缺点,sql 和代码都耦合在一块儿,最好仍是 sql 是单独一个文件,migration 脚本是一个文件,这样若是特殊状况下,方便直接在 MySQL 中运行这些 sql 文件。这时候,能够用db-migrate来代替 typeorm 来管理 migration 脚本,db-migrate 会在 migration 目录下面生成一个 js 脚本和两个 sql 文件,这两个 sql 文件一个是up
的 sql,一个是down
的 sql。
对于已有项目,若是根据数据库从头开始建立对应的 entity 是一件很麻烦的事情,这时候,可使用typeorm-model-generator来自动生成这些 entity 。好比运行下面命令:
$ typeorm-model-generator -h 127.0.0.1 -d arya -p 3310 -u root -x 123456 -e mysql -d test -o 'src/entities/' --noConfig true --cf param --ce pascal
复制代码
这时候就会在src/entities/
下面生成cat.ts
的 entity 文件:
import {BaseEntity,Column,Entity,Index,JoinColumn,JoinTable,ManyToMany,ManyToOne,OneToMany,OneToOne,PrimaryColumn,PrimaryGeneratedColumn,RelationId} from "typeorm";
@Entity("cat",{schema:"test", database:"test" } )
export class Cat {
@PrimaryGeneratedColumn({
type:"int",
name:"id"
})
id:number;
@Column("varchar",{
nullable:false,
length:50,
name:"name"
})
name:string;
@Column("int",{
nullable:false,
name:"age"
})
age:number;
@Column("varchar",{
nullable:true,
length:100,
name:"breed"
})
breed:string | null;
}
复制代码
官方给出了日志的解决方案,不过这里咱们参照nestify,使用log4js作日志处理。主要缘由是 log4js 对日志进行了分级、分盘和落盘,方便咱们更好地管理日志。
在 log4js 中日志分为九个等级:
export enum LoggerLevel {
ALL = 'ALL',
MARK = 'MARK',
TRACE = 'TRACE',
DEBUG = 'DEBUG',
INFO = 'INFO',
WARN = 'WARN',
ERROR = 'ERROR',
FATAL = 'FATAL',
OFF = 'OFF',
}
复制代码
ALL
和OFF
这两个等级通常不会直接在业务代码中使用。剩下的七个即分别对应 Logger
实例的七个方法,也就是说,在调用这些方法的时候,就至关于为这些日志定了级。
对于不一样的日志级别,在 log4js 中经过不一样颜色输出,而且输出时候带上日志输出时间和对应的 module name:
Log4js.addLayout('Awesome-nest', (logConfig: any) => {
return (logEvent: Log4js.LoggingEvent): string => {
let moduleName: string = ''
let position: string = ''
const messageList: string[] = []
logEvent.data.forEach((value: any) => {
if (value instanceof ContextTrace) {
moduleName = value.context
if (value.lineNumber && value.columnNumber) {
position = `${value.lineNumber}, ${value.columnNumber}`
}
return
}
if (typeof value !== 'string') {
value = Util.inspect(value, false, 3, true)
}
messageList.push(value)
})
const messageOutput: string = messageList.join(' ')
const positionOutput: string = position ? ` [${position}]` : ''
const typeOutput: string = `[${ logConfig.type }] ${logEvent.pid.toString()} - `
const dateOutput: string = `${Moment(logEvent.startTime).format( 'YYYY-MM-DD HH:mm:ss', )}`
const moduleOutput: string = moduleName
? `[${moduleName}] `
: '[LoggerService] '
let levelOutput: string = `[${logEvent.level}] ${messageOutput}`
switch (logEvent.level.toString()) {
case LoggerLevel.DEBUG:
levelOutput = Chalk.green(levelOutput)
break
case LoggerLevel.INFO:
levelOutput = Chalk.cyan(levelOutput)
break
case LoggerLevel.WARN:
levelOutput = Chalk.yellow(levelOutput)
break
case LoggerLevel.ERROR:
levelOutput = Chalk.red(levelOutput)
break
case LoggerLevel.FATAL:
levelOutput = Chalk.hex('#DD4C35')(levelOutput)
break
default:
levelOutput = Chalk.grey(levelOutput)
break
}
return `${Chalk.green(typeOutput)}${dateOutput} ${Chalk.yellow( moduleOutput, )}${levelOutput}${positionOutput}`
}
})
复制代码
在 log4js 中,日志的出口问题(即日志输出到哪里)由 Appender 来解决:
Log4js.configure({
appenders: {
console: {
type: 'stdout',
layout: { type: 'Awesome-nest' },
},
},
categories: {
default: {
appenders: ['console'],
level: 'debug',
},
},
})
复制代码
config 中配置了debug
级别以上的日志会经过console
输出。
接下来就是export
一个 log class
,对外暴露出 log4js 中不一样等级的 log 方法以供调用,完整代码以下:
import * as _ from 'lodash'
import * as Path from 'path'
import * as Log4js from 'log4js'
import * as Util from 'util'
import * as Moment from 'moment'
import * as StackTrace from 'stacktrace-js'
import Chalk from 'chalk'
export enum LoggerLevel {
ALL = 'ALL',
MARK = 'MARK',
TRACE = 'TRACE',
DEBUG = 'DEBUG',
INFO = 'INFO',
WARN = 'WARN',
ERROR = 'ERROR',
FATAL = 'FATAL',
OFF = 'OFF',
}
export class ContextTrace {
constructor( public readonly context: string, public readonly path?: string, public readonly lineNumber?: number, public readonly columnNumber?: number, ) {}
}
Log4js.addLayout('Awesome-nest', (logConfig: any) => {
return (logEvent: Log4js.LoggingEvent): string => {
let moduleName: string = ''
let position: string = ''
const messageList: string[] = []
logEvent.data.forEach((value: any) => {
if (value instanceof ContextTrace) {
moduleName = value.context
if (value.lineNumber && value.columnNumber) {
position = `${value.lineNumber}, ${value.columnNumber}`
}
return
}
if (typeof value !== 'string') {
value = Util.inspect(value, false, 3, true)
}
messageList.push(value)
})
const messageOutput: string = messageList.join(' ')
const positionOutput: string = position ? ` [${position}]` : ''
const typeOutput: string = `[${ logConfig.type }] ${logEvent.pid.toString()} - `
const dateOutput: string = `${Moment(logEvent.startTime).format( 'YYYY-MM-DD HH:mm:ss', )}`
const moduleOutput: string = moduleName
? `[${moduleName}] `
: '[LoggerService] '
let levelOutput: string = `[${logEvent.level}] ${messageOutput}`
switch (logEvent.level.toString()) {
case LoggerLevel.DEBUG:
levelOutput = Chalk.green(levelOutput)
break
case LoggerLevel.INFO:
levelOutput = Chalk.cyan(levelOutput)
break
case LoggerLevel.WARN:
levelOutput = Chalk.yellow(levelOutput)
break
case LoggerLevel.ERROR:
levelOutput = Chalk.red(levelOutput)
break
case LoggerLevel.FATAL:
levelOutput = Chalk.hex('#DD4C35')(levelOutput)
break
default:
levelOutput = Chalk.grey(levelOutput)
break
}
return `${Chalk.green(typeOutput)}${dateOutput} ${Chalk.yellow( moduleOutput, )}${levelOutput}${positionOutput}`
}
})
Log4js.configure({
appenders: {
console: {
type: 'stdout',
layout: { type: 'Awesome-nest' },
},
},
categories: {
default: {
appenders: ['console'],
level: 'debug',
},
},
})
const logger = Log4js.getLogger()
logger.level = LoggerLevel.TRACE
export class Logger {
static trace(...args) {
logger.trace(Logger.getStackTrace(), ...args)
}
static debug(...args) {
logger.debug(Logger.getStackTrace(), ...args)
}
static log(...args) {
logger.info(Logger.getStackTrace(), ...args)
}
static info(...args) {
logger.info(Logger.getStackTrace(), ...args)
}
static warn(...args) {
logger.warn(Logger.getStackTrace(), ...args)
}
static warning(...args) {
logger.warn(Logger.getStackTrace(), ...args)
}
static error(...args) {
logger.error(Logger.getStackTrace(), ...args)
}
static fatal(...args) {
logger.fatal(Logger.getStackTrace(), ...args)
}
static getStackTrace(deep: number = 2): ContextTrace {
const stackList: StackTrace.StackFrame[] = StackTrace.getSync()
const stackInfo: StackTrace.StackFrame = stackList[deep]
const lineNumber: number = stackInfo.lineNumber
const columnNumber: number = stackInfo.columnNumber
const fileName: string = stackInfo.fileName
const extnameLength: number = Path.extname(fileName).length
let basename: string = Path.basename(fileName)
basename = basename.substr(0, basename.length - extnameLength)
const context: string = _.upperFirst(_.camelCase(basename))
return new ContextTrace(context, fileName, lineNumber, columnNumber)
}
}
复制代码
这样在须要输出日志的地方只要这样调用就行:
Logger.info(id)
复制代码
但是咱们并不但愿每一个请求都本身打 log,这时候能够把这个 log 做为中间件:
import { Logger } from '../../shared/utils/logger'
export function logger(req, res, next) {
const statusCode = res.statusCode
const logFormat = `${req.method} ${req.originalUrl} ip: ${req.ip} statusCode: ${statusCode}`
next()
if (statusCode >= 500) {
Logger.error(logFormat)
} else if (statusCode >= 400) {
Logger.warn(logFormat)
} else {
Logger.log(logFormat)
}
}
复制代码
在main.ts
中注册:
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.setGlobalPrefix('api/v1')
app.use(logger)
app.useGlobalFilters(new ExceptionsFilter())
app.useGlobalInterceptors(new TransformInterceptor())
app.useGlobalPipes(new ValidationPipe())
await app.listen(config.port, config.hostName)
}
复制代码
而且在ExceptionsFilter
中也对捕捉到的 Exception 进行日志输出:
export class ExceptionsFilter implements ExceptionFilter {
async catch(exception, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const response = ctx.getResponse()
const request = ctx.getRequest()
Logger.error('exception', JSON.stringify(exception))
let message = exception.message
let isDeepestMessage = false
while (!isDeepestMessage) {
isDeepestMessage = !message.message
message = isDeepestMessage ? message : message.message
}
const errorResponse = {
message: message || '请求失败',
status: 1,
}
const status = exception instanceof HttpException ?
exception.getStatus() :
HttpStatus.INTERNAL_SERVER_ERROR
Logger.error(
`Catch http exception at ${request.method} ${request.url} ${status}`,
)
response.status(status)
response.header('Content-Type', 'application/json; charset=utf-8')
response.send(errorResponse)
}
}
复制代码
这样一个基础的日志输出系统差很少就完成了。固然,log4js 的appender
还支持下面几种:
DateFile:日志输出到文件,日志文件能够安特定的日期模式滚动,例现在天输出到 default-2016-08-21.log
,明天输出到 default-2016-08-22.log
;
SMTP:输出日志到邮件;
Mailgun:经过 Mailgun API 输出日志到 Mailgun;
levelFilter 能够经过 level 过滤;
等等其余一些 appender,到这里能够看到所有的列表。
好比,下面配置就会把日志输出到加上日期后缀的文件中,而且保留 60 天:
Log4js.configure({
appenders: {
fileAppender: {
type: 'DateFile',
filename: './logs/prod.log',
pattern: '-yyyy-MM-dd.log',
alwaysIncludePattern: true,
layout: { type: 'Flash' },
daysToKeep: 60
}
},
categories: {
default: {
appenders: ['fileAppender'],
level: 'info'
}
},
})
复制代码
对于通常的 CRUD 的操做,在 Nestjs 中可使用@nestjsx/crud这个库来帮咱们减小开发量。
首先安装相关依赖:
npm i @nestjsx/crud @nestjsx/crud-typeorm class-transformer class-validator --save
复制代码
而后新建dog.entity.ts
:
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm'
@Entity('dog')
export class DogEntity {
@PrimaryGeneratedColumn()
id: number
@Column({ length: 50 })
name: string
@Column()
age: number
@Column({ length: 100, nullable: true })
breed: string
}
复制代码
在dog.service.ts
中只需写下面几行代码:
import { Injectable } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { TypeOrmCrudService } from '@nestjsx/crud-typeorm'
import { DogEntity } from './dog.entity'
@Injectable()
export class DogsService extends TypeOrmCrudService<DogEntity> {
constructor(@InjectRepository(DogEntity) repo) {
super(repo)
}
}
复制代码
在dog.controller.ts
中,使用@crud
帮助自动生成API:
import { Controller } from '@nestjs/common'
import { Crud, CrudController } from '@nestjsx/crud'
import { DogEntity } from './dog.entity'
import { DogsService } from './dogs.service'
@Crud({
model: {
type: DogEntity,
},
})
@Controller('dogs')
export class DogsController implements CrudController<DogEntity> {
constructor(public service: DogsService) {}
}
复制代码
这时候,就能够按照@nestjsx/crud的文档中 API 规则去请求对应的 CRUD 的操做。好比,请求GET api/v1/dogs
,就会返回全部 dog
的数组;请求GET api/v1/dogs/1
,就会返回 id
为1
的 dog
。