参考
资源
前言 此文是对 转型计划之初级后端(中) 的整理与补充, 措辞尽量精简以方便后续查阅。
前置知识
初始化项目 使用 npm 全局安装一些命令行工具
1 2 3 npm install -g @nestjs/cli npm install -g yarn npm install -g yarn-upgrade -all
使用 nest-cli 初始化项目, 包管理器选 yarn
1 2 nest new desktop-deployment -management -tool -api cd desktop-deployment -management -tool -api
使用 yarn-upgrade-all 更新依赖
修改 prettier 配置文件 .prettierrc
1 2 3 4 5 { "singleQuote" : true , "trailingComma" : "all" , "semi" : false }
创建编辑器工作区配置
1 mkdir .vscode;touch .vscode/setting.json
配置 prettier 的 vscode 插件: prettier-vscode
setting.json
1 2 3 4 5 6 { "[typescript]" : { "editor.defaultFormatter" : "esbenp.prettier-vscode" , "editor.formatOnSave" : true }, }
使用 nest-cli 生成 Interceptors, Exception filters, Pipes, Guards
1 2 3 4 nest generate interceptor common/interceptors/logging nest generate filter common/filters/all-exception nest generate pipe common/pipes/body-validation nest generate guard common/guard/auth
运行 jest 测试
配置 使用 yarn 添加运行时依赖 config 和 dotenv
创建并编辑 config 配置文件
1 2 3 4 5 6 mkdir config touch config/default.json touch config/production.json touch config/custom-environment -variables .json touch src/config.ts touch .env
default.json
1 2 3 4 5 6 7 8 9 10 11 { "env" : "development" , "port" : 3000 , "logLevel" : "debug" , "db" : { "name" : "ddmtdb" , "password" : "rootpass" , "port" : 3307 , "username" : "root" } }
production.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { "env" : "development" , "port" : 3000 , "logLevel" : "debug" , "db" : { "host" : "db" , "name" : "ddmtdb" , "password" : "rootpass" , "port" : 3306 , "username" : "root" }, "jwt" : { "secret" : "secret-key" } }
custom-environment-variables.json
1 2 3 4 5 6 7 8 9 10 11 { "env" : "NODE_ENV" , "port" : "PORT" , "logLevel" : "LOG_LEVEL" , "db" : { "name" : "DB_NAME" , "password" : "DB_PASSWORD" , "port" : "DB_PORT" , "username" : "DB_USERNAME" } }
config.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import * as dotenv from "dotenv" dotenv.config() export interface Config { env : string port : number logLevel : string db : { name : string username : string password : string port : number } } export const config: Config = require ('config' )
.env
1 2 3 4 5 6 7 8 9 10 NODE_ENV=development PORT=3000 SECRET=secret-key LOG_LEVEL=debug DB_NAME=ddmtdb DB_USERNAME=root DB_PASSWORD=rootpass DB_PORT=3307 ADMINER_THEME=pepa-linha ADMINER_PORT=8081
.env 中的值映射到 custom-environment-variables.json 对应的值上, 而 custom-environment-variables.json 中的值映射到 default.json 对应的值上. 最终 .env 中的值覆盖 default.json 中对应的值.
加密敏感信息请参考: 使用 Git-crypt 加密生产配置文件
日志 使用 yarn 添加运行时依赖 nest-winston 和 winston
1 yarn add nest-winston winston
config.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import * as dotenv from 'dotenv' import { utilities as nestWinstonModuleUtilities } from 'nest-winston' import { format, transports } from 'winston' dotenv.config() export interface Config { env : string port : number logLevel : string dbName : string dbPassword : string dbPort : number } export const config: Config = require ('config' )export const loggerConfig = { level : config.logLevel, transports : [ new transports.Console({ format : format.combine( format.colorize(), nestWinstonModuleUtilities.format.nestLike(), ), }), new transports.File({ filename : 'combined.log' , level : config.logLevel, }), ], }
logging.interceptor.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import { Injectable, NestInterceptor, ExecutionContext, CallHandler, LoggerService, } from '@nestjs/common' import { Observable } from 'rxjs' import { tap } from 'rxjs/operators' import { Request, Response } from 'express' @Injectable ()export class LoggingInterceptor implements NestInterceptor { constructor (private readonly logger: LoggerService ) {} intercept(context: ExecutionContext, next : CallHandler): Observable<any > { const ctx = context.switchToHttp() const response = ctx.getResponse<Response>() const request = ctx.getRequest<Request>() const method = request.method const url = request.url const requestTime = Date .now() request.params.requestTime = Date .now().toString() return next .handle() .pipe( tap(() => this .logger.log( `${method} ${url} - ${response.statusCode} - ${Date .now() - requestTime} ms` , ), ), ) } }
main.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { NestFactory } from '@nestjs/core' import { AppModule } from './app.module' import { WINSTON_MODULE_NEST_PROVIDER, WinstonModule } from 'nest-winston' import { LoggingInterceptor } from './common/interceptors/logging.interceptor' import { config, loggerConfig } from './config' const logger = WinstonModule.createLogger(loggerConfig)async function bootstrap ( ) { const app = await NestFactory.create(AppModule) app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER)) app.useGlobalInterceptors(new LoggingInterceptor(logger)) await app.listen(config.port) logger.log(`Server started on port ${config.port} ` ) } bootstrap()
app.module.ts
1 2 3 4 5 6 7 8 9 10 11 12 import { Module } from '@nestjs/common' import { AppController } from './app.controller' import { AppService } from './app.service' import { WinstonModule } from 'nest-winston' import { loggerConfig } from './config' @Module ({ imports : [WinstonModule.forRoot(loggerConfig)], controllers : [AppController], providers : [AppService], }) export class AppModule {}
异常捕获 all-exception.filter.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, LoggerService, } from '@nestjs/common' import { Request, Response } from 'express' @Catch ()export class AllExceptionFilter implements ExceptionFilter { constructor (private readonly logger: LoggerService ) {} catch (exception: HttpException | Error , host : ArgumentsHost) { const ctx = host.switchToHttp() const response = ctx.getResponse<Response>() const request = ctx.getRequest<Request>() const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR const stackTop = exception.stack .split('\n' )[1 ] .split('at ' )[1 ] .split(' ' )[0 ] const message = exception.message.message || exception.message const meta = exception.message.meta || exception.message.errors const logMessage = { status, message, meta, } this .logger.error(JSON .stringify(logMessage), stackTop, 'TRACE' ) const method = request.method const url = request.url const requestTime = request.params.requestTime this .logger.log( `${method} ${url} - ${status} - ${Date .now() - Number (requestTime)} ms` , 'Access' , ) response.status(status).send(logMessage) } }
main.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import { NestFactory } from '@nestjs/core' import { AppModule } from './app.module' import { WINSTON_MODULE_NEST_PROVIDER, WinstonModule } from 'nest-winston' import { LoggingInterceptor } from './common/interceptors/logging.interceptor' import { AllExceptionFilter } from './common/filters/all-exception.filter' import { config, loggerConfig } from './config' const logger = WinstonModule.createLogger(loggerConfig)async function bootstrap ( ) { const app = await NestFactory.create(AppModule) app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER)) app.useGlobalInterceptors(new LoggingInterceptor(logger)) app.useGlobalFilters(new AllExceptionFilter(logger)) await app.listen(config.port) logger.log(`Server started on port ${config.port} ` ) } bootstrap()
请求体验证 使用 yarn 添加运行时依赖 class-validator 和 class-transformer
1 yarn add class -validator class -transformer
body-validation.pipe.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException, } from '@nestjs/common' import { validate } from 'class-validator' import { plainToClass } from 'class-transformer' @Injectable ()export class ValidationPipe implements PipeTransform <any > { async transform (value: any , metadata: ArgumentMetadata ) { if (value == null ) { value = {} } 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 topLevelErrors = errors .filter(v => v.constraints) .map(error => { return { property : error.property, constraints : Object .values(error.constraints), } }) const nestedErrors = errors .filter(v => !v.constraints) .map(error => { const validationErrors = this .getValidationErrorsFromChildren( error.property, error.children, ) return validationErrors }) throw new BadRequestException({ message : 'Validation failed' , meta : topLevelErrors.concat(...nestedErrors), }) } return value } private toValidate(metatype: any ): boolean { const types: Function [] = [String , Boolean , Number , Array , Object ] return !types.includes(metatype) } private getValidationErrorsFromChildren (parent, children, errors = [] ) { children.forEach(child => { if (child.constraints) { errors.push({ property : `${parent} .${child.property} ` , constraints : Object .values(child.constraints), }) } else { return this .getValidationErrorsFromChildren( `${parent} .${child.property} ` , child.children, errors, ) } }) return errors } }
main.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { NestFactory } from '@nestjs/core' import { AppModule } from './app.module' import { WINSTON_MODULE_NEST_PROVIDER, WinstonModule } from 'nest-winston' import { LoggingInterceptor } from './common/interceptors/logging.interceptor' import { AllExceptionFilter } from './common/filters/all-exception.filter' import { BodyValidationPipe } from './common/pipes/body-validation.pipe' import { config, loggerConfig } from './config' const logger = WinstonModule.createLogger(loggerConfig)async function bootstrap ( ) { const app = await NestFactory.create(AppModule) app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER)) app.useGlobalInterceptors(new LoggingInterceptor(logger)) app.useGlobalFilters(new AllExceptionFilter(logger)) app.useGlobalPipes(new BodyValidationPipe()) await app.listen(config.port) logger.log(`Server started on port ${config.port} ` ) } bootstrap()
数据库 创建文件
1 2 mkdir mysql touch docker-compose .yml
docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 version: '3.1' services: db: image: mysql command: --default-authentication-plugin=mysql_native_password restart: always environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} MYSQL_DATABASE: ${DB_NAME} ports: - "${DB_PORT}:3306" volumes: - ./mysql:/var/lib/mysql adminer: image: adminer restart: always environment: ADMINER_DESIGN: ${ADMINER_THEME} ports: - ${ADMINER_PORT}:8080
package.json
1 2 3 4 5 { "scripit" : { "db" : "docker-compose up -d" } }
生成并运行容器
打开 http://127.0.0.1:8081/ 登录即可
参考: typeorm docs 以及 https://docs.nestjs.com/techniques/database
添加运行时依赖 typeorm 和 @nestjs/typeorm 和 mysql
1 yarn add typeorm @nestjs/typeorm mysql
config.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import * as dotenv from 'dotenv' import { utilities as nestWinstonModuleUtilities } from 'nest-winston' import { format, transports } from 'winston' import { TypeOrmModuleOptions } from '@nestjs/typeorm' dotenv.config() export const isDev = process.env.NODE_ENV === 'development' export interface Config { env : string port : number logLevel : string db : { host : string name : string username : string password : string port : number } jwt : { secret : string } } export const config: Config = require ('config' )export const loggerConfig = { level : config.logLevel, transports : [ new transports.Console({ format : format.combine( format.colorize(), format.timestamp(), nestWinstonModuleUtilities.format.nestLike(), ), }), new transports.File({ filename : 'combined.log' , level : config.logLevel, }), ], } export const dbConfig: TypeOrmModuleOptions = { type : 'mysql' , name : 'default' , host : config.db.host, port : config.db.port, username : config.db.username, password : config.db.password, database : config.db.name, entities : ['dist/**/**.entity{.ts,.js}' ], synchronize : isDev ? true : false , }
app.module.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { Module } from '@nestjs/common' import { AppController } from './app.controller' import { AppService } from './app.service' import { WinstonModule } from 'nest-winston' import { TypeOrmModule } from '@nestjs/typeorm' import { loggerConfig, dbConfig } from './config' @Module ({ imports : [ TypeOrmModule.forRoot(dbConfig), WinstonModule.forRoot(loggerConfig), ], controllers : [AppController], providers : [AppService], }) export class AppModule {}
swagger 文档 参考: https://docs.nestjs.com/recipes/swagger#openapi-swagger 添加运行时依赖 @nestjs/swagger swagger-ui-express
1 yarn add @nestjs/swagger swagger-ui -express
main.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 import { NestFactory } from '@nestjs/core' import { AppModule } from './app.module' import { WINSTON_MODULE_NEST_PROVIDER, WinstonModule } from 'nest-winston' import { LoggingInterceptor } from './common/interceptors/logging.interceptor' import { AllExceptionFilter } from './common/filters/all-exception.filter' import { BodyValidationPipe } from './common/pipes/body-validation.pipe' import { config, loggerConfig } from './config' import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger' const logger = WinstonModule.createLogger(loggerConfig)async function bootstrap ( ) { const app = await NestFactory.create(AppModule) app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER)) app.useGlobalInterceptors(new LoggingInterceptor(logger)) app.useGlobalFilters(new AllExceptionFilter(logger)) app.useGlobalPipes(new BodyValidationPipe()) const options = new DocumentBuilder() .setTitle('NestJS Realworld Example App' ) .setDescription('The Realworld API description' ) .setVersion('1.0' ) .addBearerAuth() .build() const document = SwaggerModule.createDocument(app, options) SwaggerModule.setup('/docs' , app, document ) await app.listen(config.port) logger.log(`Server started on port ${config.port} ` ) } bootstrap()
认证 参考: passport docs 以及 https://docs.nestjs.com/techniques/authentication 添加运行时依赖 [@nestjs/passport][] 和 passport 和 [passport-local][] 和 [@nestjs/jwt][] 和 [passport-jwt][] 添加开发时依赖 [@types/passport-local][] 和 [@types/passport-jwt][]
1 2 yarn add @nestjs/passport passport passport-local @nestjs/jwt passport-jwt yarn add @types/passport-local @types/passport-jwt --save -dev
生成 auth 与 users 模块文件
1 2 3 4 5 6 7 nest g module auth nest g service auth nest g module user nest g service user nest g controller user touch src/auth/local.strategy.ts touch src/auth/jwt.strategy.ts
user user.entity.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import { Entity, PrimaryGeneratedColumn, Column, BeforeInsert } from 'typeorm' import { IsEmail } from 'class-validator' import * as crypto from 'crypto' @Entity ('user' )export class UserEntity { @PrimaryGeneratedColumn () id : number @Column () username : string @Column () email : string @Column ({ default : '' }) bio : string @Column ({ default : '' }) image : string @Column () password : string @BeforeInsert () hashPassword ( ) { this .password = crypto.createHmac('sha256' , this .password).digest('hex' ) } }
user.decorator.ts
1 2 3 4 5 6 7 8 import { createParamDecorator } from '@nestjs/common' export const User = createParamDecorator((data, req ) => { if (req.user) { return data ? req.user[data] : req.user } })
user.controller.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 import { Get, Post, Body, Put, Delete, Param, Controller, UseGuards, Request, } from '@nestjs/common' import { UserService } from './user.service' import { UserRO } from './user.interface' import { CreateUserDto, UpdateUserDto } from './dto' import { User } from './user.decorator' import { AuthGuard } from '@nestjs/passport' import { AuthService } from '../auth/auth.service' import { ApiTags, ApiBearerAuth, ApiBody } from '@nestjs/swagger' @ApiBearerAuth ()@ApiTags ('user' )@Controller ()export class UserController { constructor ( private readonly userService: UserService, private readonly authService: AuthService, ) {} @UseGuards (AuthGuard('jwt' )) @Put ('user/profile' ) async update (@User ('email' ) email: string , @Body () userData: UpdateUserDto ) { const newUser = await this .userService.update(email, userData) return this .authService.buildUserRO(newUser) } @Post ('users/sign_up' ) @ApiBody ({ type : CreateUserDto }) async create (@Body () userData: CreateUserDto ) { const newUser = await this .userService.create(userData) return this .authService.buildUserRO(newUser) } @UseGuards (AuthGuard('jwt' )) @Delete ('user/cancel' ) async delete (@User ('email' ) email: string ) { return await this .userService.delete(email) } @UseGuards (AuthGuard('local' )) @Post ('user/sign_in' ) async login(@Request () req): Promise <any > { return this .authService.buildUserRO(req.user) } }
user.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 import { Injectable, HttpStatus } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { Repository, getRepository, DeleteResult } from 'typeorm' import { UserEntity } from './user.entity' import { CreateUserDto, LoginUserDto, UpdateUserDto } from './dto' import { config } from '../config' import { UserRO } from './user.interface' import { validate } from 'class-validator' import { HttpException } from '@nestjs/common/exceptions/http.exception' import * as crypto from 'crypto' import * as jwt from 'jsonwebtoken' @Injectable ()export class UserService { constructor ( @InjectRepository (UserEntity) private readonly userRepository: Repository<UserEntity>, ) {} async findAll(): Promise <UserEntity[]> { return await this .userRepository.find() } async findOne(loginUserDto: LoginUserDto): Promise <UserEntity> { const findOneOptions = { email : loginUserDto.email, password : crypto .createHmac('sha256' , loginUserDto.password) .digest('hex' ), } const user = await this .userRepository.findOne(findOneOptions) return user } async create(dto: CreateUserDto): Promise <any > { const { username, email, password } = dto const qb = await getRepository(UserEntity) .createQueryBuilder('user' ) .where('user.username = :username' , { username }) .orWhere('user.email = :email' , { email }) const user = await qb.getOne() if (user) { const errors = { username : 'Username and email must be unique.' } throw new HttpException( { message : 'Input data validation failed' , errors }, HttpStatus.BAD_REQUEST, ) } const newUser = new UserEntity() newUser.username = username newUser.email = email newUser.password = password const errors = await validate(newUser) if (errors.length > 0 ) { const _errors = { username : 'Userinput is not valid.' } throw new HttpException( { message : 'Input data validation failed' , meta : _errors }, HttpStatus.BAD_REQUEST, ) } else { const savedUser = await this .userRepository.save(newUser) return savedUser } } async update(email: string , dto : UpdateUserDto): Promise <UserEntity> { const toUpdate = await this .userRepository.findOne(email) delete toUpdate.password const updated = Object .assign(toUpdate, dto) return await this .userRepository.save(updated) } async delete (email: string ): Promise <DeleteResult> { return await this .userRepository.delete({ email : email }) } }
user.module.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { Module, forwardRef } from '@nestjs/common' import { UserController } from './user.controller' import { TypeOrmModule } from '@nestjs/typeorm' import { UserEntity } from './user.entity' import { UserService } from './user.service' import { AuthModule } from '../auth/auth.module' @Module ({ imports : [ TypeOrmModule.forFeature([UserEntity]), forwardRef(() => AuthModule), ], providers : [UserService], controllers : [UserController], exports : [UserService], }) export class UserModule {}
auth auth.module.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import { Module, forwardRef } from '@nestjs/common' import { AuthService } from './auth.service' import { LocalStrategy } from './local.strategy' import { JwtStrategy } from './jwt.strategy' import { UserModule } from '../user/user.module' import { PassportModule } from '@nestjs/passport' import { JwtModule } from '@nestjs/jwt' import { config } from '../config' @Module ({ imports : [ forwardRef(() => UserModule), PassportModule, JwtModule.register({ secret : config.jwt.secret, signOptions : { expiresIn : '60s' }, }), ], providers : [AuthService, LocalStrategy, JwtStrategy], exports : [AuthService], }) export class AuthModule {}
auth.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import { Injectable, Inject } from '@nestjs/common' import { UserService } from '../user/user.service' import { JwtService } from '@nestjs/jwt' import { LoginUserDto, UserRO } from '../user/dto' @Injectable ()export class AuthService { constructor ( private readonly userService: UserService, private readonly jwtService: JwtService, ) {} async validateUser({ email, password }: LoginUserDto): Promise <any > { const user = await this .userService.findOne({ email, password }) if (user) { const { id, email, username, bio, image } = user return { id, email, username, bio, image } } return null } async buildUserRO(user): Promise <UserRO> { const { id, email, username, bio, image } = user const payload = { id, email, username } return { username, email, bio, image, access_token : this .jwtService.sign(payload), } } }
local.strategy local.strategy.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import { Strategy } from 'passport-local' import { PassportStrategy } from '@nestjs/passport' import { Injectable, UnauthorizedException } from '@nestjs/common' import { AuthService } from './auth.service' @Injectable ()export class LocalStrategy extends PassportStrategy (Strategy ) { constructor (private readonly authService: AuthService ) { super ({ usernameField : 'email' , passwordField : 'password' , session : false , }) } async validate(email, password): Promise <any > { const user = await this .authService.validateUser({ email, password, }) if (!user) { throw new UnauthorizedException({ error : 'Incorrect username or password' , }) } return user } }
jwt.strategy jwt.strategy.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import { ExtractJwt, Strategy } from 'passport-jwt' import { PassportStrategy } from '@nestjs/passport' import { Injectable } from '@nestjs/common' import { config } from '../config' import { JwtPayload } from '../user/dto' @Injectable ()export class JwtStrategy extends PassportStrategy (Strategy ) { constructor ( ) { super ({ jwtFromRequest : ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration : false , secretOrKey : config.jwt.secret, }) } validate(payload: JwtPayload): JwtPayload { return payload } }
Dockerizing 参考: 把一个 Node.js web 应用程序给 Docker 化 新建文件
1 2 3 touch Dockerfile touch .dockerignore touch docker-compose .prod.yml
.env
1 2 3 4 5 6 7 8 9 10 11 NODE_ENV=production PORT=3000 SECRET=secret-key LOG_LEVEL=debug # DB_HOST=db DB_NAME=ddmtdb DB_USERNAME=root DB_PASSWORD=rootpass DB_PORT=3306 ADMINER_THEME=pepa-linha ADMINER_PORT=8080
.dockerignore
1 2 3 node_modules npm-debug.log yarn-error.log
Dockerfile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 FROM node:10 WORKDIR /usr/src/app COPY package*.json ./ RUN apt-get update && apt-get install -y build-essential && apt-get install -y python RUN npm install FROM node:10 -alpineWORKDIR /usr/src/app COPY --from=0 /usr/src/app . COPY . . EXPOSE 3000 CMD [ "npm" , "run" , "start:prod" ]
docker-compose.prod.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 version: '3.7' services: nest-web-api: build: . restart: always ports: - "3000:3000" depends_on: - db - adminer db: image: mysql command: --default-authentication-plugin=mysql_native_password restart: always environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} MYSQL_DATABASE: ${DB_NAME} ports: - "${DB_PORT}:3306" volumes: - ./mysql:/var/lib/mysql adminer: image: adminer restart: always environment: ADMINER_DESIGN: ${ADMINER_THEME} ports: - ${ADMINER_PORT}:8080
package.json script
1 2 3 4 5 { "start:prod" : "npm run build && node dist/main" , "db" : "docker-compose up -d" , "deploy" : "docker-compose -f docker-compose.prod.yml up" }
README.md
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ## Dockerizing ### development .env ``` NODE_ENV=development ``` ```bash yarn db ``` ### deploy .env ``` NODE_ENV=production ``` ```bash yarn deploy ```
扩展阅读
结语 到这种程度就足够支撑基本的单体架构应用的开发了, 更多的应用优化(如缓存, 日志监控分析)与开发概念(如 DDD, microservices, serverless),则需要到业务中去沉淀一番才能有所总结。 再进一步: [DDD and Microservices][]