This article is based on the Middle layer of Node implemented by Nest+Emp2. A framework that works out of the box. For user login (including wechat scan login), authentication, middleware, service aggregation, rendering, Session, Cache and other functions, it also has the Emp micro front end feature.

What is the BFF layer

BFF(back-end For front-end) is a middle-tier concept, which is a layer of NodeJS, capable of forwarding requests and converting data. Node works with both the front-end technology stack and is better suited for concurrent requests to microservices. Can also be made to the forward Restful, after the implementation of RPC; You can also add Cache, authentication, middleware, service aggregation, rendering, and so on to BFF, which can be modified according to your own needs.

Meet Nest


Nest is a framework for building efficient, scalable Node.js server-side applications. It uses progressive JavaScript, has built-in and full TypeScript support (but still allows developers to write code in pure JavaScript), and combines OOP (object-oriented programming), FP (functional programming), and FRP (functional response programming) elements at the bottom. Nest uses powerful HTTP Server frameworks such as Express (the default) and Fastify. Nest provides a level of abstraction on top of these frameworks, while also exposing its APIS directly to developers. This makes it easy to use countless third-party modules for each platform.

So much for Nest.

Micro Emp front-end


It is based on the construction of the next generation to realize the micro front end solution, combined with webpack5, Module Federation rich project practice, to establish a three-layer sharing model. Emp2 website

Nest + Emp integration

How to make two different projects run together, how to implement hot update, how to request data injection into Window in Node layer, how to obtain EMP compilation file locally, etc.

Build the project

Start by building nest projects and EMP projects locally

// nest
$ npm i -g @nestjs/cli 
$ nest new project-name
// emp 
$ npm i -g @efox/emp
$ npm init // Select the building module
Copy the code

How to support hot updates and local Node access to files

First let’s take a look at the emp-config.js configuration information

module.exports = {
  webpack() {
    return {
      devServer: {
        port: 8081,}}},webpackChain(config) {
    config.plugin('html').tap(args= > {
      args[0] = {
        ...args[0].template: resolve('./views/index.html'),
        ...{
          title: 'demo'.files: {},}},return args
    })
  },
  moduleFederation: {
    name: 'empReact'.filename: 'emp.js'.remotes: {},
    exposes: {
      './App': 'src/App',},shared: {
      react: {eager: true.singleton: true.requiredVersion: '^ 17.0.1'},
      'react-dom': {eager: true.singleton: true.requiredVersion: '^ 17.0.1'},
      'react-router-dom': {requiredVersion: '^ 5.1.2'}},}},Copy the code

Solve Webpack local compilation hot update file problem, need to Webpack devServer for processing.

server: {
  port: 8001.devMiddleware: {
    index: true.mimeTypes: { phtml: 'text/html' },
    publicPath: './dist/client'.serverSideRender: true.writeToDisk: true,}},Copy the code

Modify the path of the HTML reference file, and modify the exported path.

html: {
  template: resolve('./views/index.html'),
  filename: resolve('./dist/views/index.html'),
  title: 'Mark camera'
},
Copy the code

Injection of the service’s request interface into the Window object requires special handling. Because the EJS template engine is used for Webpack injection code, writing EJS code in HTML will be resolved at Webpack compilation time. Subsequent injection fails when using service rendering. The workaround is to inject JavaScript code at compile time through the Webpack plug-in html-inline-code-plugin.

  chain.plugin('InlineCodePlugin').use(new InlineCodePlugin({
    begin: false.tag: 'script'.inject: 'body'.code: `window.INIT_DATA = <%- JSON.stringify(data) %>`
}))
Copy the code

At this point, the Emp code is almost modified, then modify the configuration to move to Nest project. The tsconfi.json file needs to be moved, and the file impact file needs to be excluded.

In response to hot updates, local development started devServer, which also compiled updates in real time through service access.

Nest Render page

Node layer rendering page has many engines such as EJS, HBS and so on, this project is using EJS template engine.

First configure the EJS template engine rendering page in Nest.

app.useStaticAssets(resolve(__dirname, '.. /.. /dist/client'))

app.setBaseViewsDir(join(__dirname, '.. /.. /dist/views'));
app.setViewEngine('html');
app.engine('html', ejs.renderFile);
Copy the code

The route configuration is as follows:

/** * render the page *@param {Request} req
* @return {*} 
* @memberof AppController* /
@Get('login')
@Render('index')
login(@QueryParams('request'.new SessionPipe()) req: Request) {
    if (req.isLogin) {
        / / redirection
        return { redirectUrl: '/'}}else {
        return { data: 121212}}}Copy the code

The page can be accessed normally.

Interface polymerization

Nest itself provides the Axios module. However, in the development project, I found that Axios did not do request interception and print logs, so I rewrote AxioModel

import logger from "@app/utils/logger";
import { UnAuthStatus } from "@app/constants/error.constant";
import { BadRequestException, HttpStatus, Injectable, UnauthorizedException } from "@nestjs/common";
import axios, { AxiosRequestConfig, AxiosResponse, CancelTokenSource, Method } from "axios";

/ * * * * don't use at https://github.com/nestjs/axios@nest/axios is calling the interface because the RXJS version is not the same@export
 * @class AxiosService* /
@Injectable(a)export class AxiosService {
    public get<T>(
        url: string, data? :any, config? : AxiosRequestConfig, ):Promise<AxiosResponse<T>> {
        return this.makeObservable<T>('get', url, data, config);
    }

    public post<T>(
        url: string, data? :any, config? : AxiosRequestConfig, ):Promise<AxiosResponse<T>> {
        return this.makeObservable<T>('post', url, data, config);
    }

    protected makeObservable<T>(
        method: Method,
        url: string.data: any, config? : AxiosRequestConfig, ):Promise<AxiosResponse<T>> {

        let axiosConfig: AxiosRequestConfig = {
            method: method,
            url,
        }

        const instance = axios.create()

        let cancelSource: CancelTokenSource;
        if(! axiosConfig.cancelToken) { cancelSource = axios.CancelToken.source(); axiosConfig.cancelToken = cancelSource.token; }// Request interception creates only one here and optimizes interception later
        instance.interceptors.request.use(
            cfg= >{ cfg.params = { ... cfg.params,ts: Date.now() / 1000 }
                return cfg
            },
            error= > Promise.reject(error)
        )

        // Response interception
        instance.interceptors.response.use(
            response= > {
                const rdata = response.data || {}
                if (rdata.code == 200 || rdata.code == 0) {
                    logger.info('Forwarding request interface succeeded =${url}, get dataThe ${JSON.stringify(rdata.result).slice(0.350)}`)
                    return rdata.result
                } else {
                    return Promise.reject({
                        msg: rdata.message || 'Forwarding interface error'.errCode: rdata.code || HttpStatus.BAD_REQUEST,
                        config: response.config
                    })
                }
            },
            error= > {
                const data = error.response && error.response.data || {}
                const msg = error.response && (data.error || error.response.statusText)
                return Promise.reject({
                    msg: msg || error.message || 'network error'.errCode: data.code || HttpStatus.BAD_REQUEST,
                    config: error.config
                })
            }
        )
        if (method === 'get') {
            axiosConfig.params = data
        } else {
            axiosConfig.data = data
        }
        if (config) {
            axiosConfig = Object.assign(axiosConfig, config)
        }
        return instance
            .request(axiosConfig)
            .then((res: any) = > res || {})
            .catch((err) = > {
                logger.error('Forward request interface =${url}, the parameter is =The ${JSON.stringify(data)}, error cause =${err.msg || 'Request error'}; Request interface status code=${err.errCode}`)
                if (UnAuthStatus.includes(err.errCode)) {
                    throw new UnauthorizedException({
                        status: err.errCode,
                        message: err.msg || err.stack
                    }, err.errCode)
                } else {
                    throw new BadRequestException({
                        isApi: true.status: err.errCode,
                        message: err.msg || err.stack
                    }, err.errCode)
                }
            })
    };
}

Copy the code

Now that you have the Http module, it’s time to deal with the interface forwarding API. The current value provides Get and Post methods for forwarding. You can implement Restful apis as required

import { QueryParams } from '@app/decorators/params.decorator';
import { Responsor } from '@app/decorators/responsor.decorator';
import { ApiGuard } from '@app/guards/api.guard';
import { HttpRequest } from '@app/interfaces/request.interface';
import { TransformPipe } from '@app/pipes/transform.pipe';
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
import { ApiService } from './api.service';

@Controller('api')
export class ApiConstroller {

    constructor(private readonly apiService: ApiService){}/** * Get interface forward *@param {HttpRequest} data
     * @return {*} 
     * @memberof ApiConstroller* /
    @UseGuards(ApiGuard)
    @Responsor.api()
    @Get('transform')
    getTransform(@QueryParams('query'.new TransformPipe()) data: HttpRequest) {
        return this.apiService.get(data)
    }

    /** * Post Interface forward *@param {HttpRequest} data
     * @return {*} 
     * @memberof ApiConstroller* /
    @UseGuards(ApiGuard)
    @Responsor.api()
    @Post('transform')
    postTransform(@Body(new TransformPipe()) data: HttpRequest) {
        return this.apiService.post(data)
    }
}
Copy the code

Cache Cache

Redis is best known for providing a distributed caching mechanism for clustered applications. Can be shared across multiple services.

  • Redis.module. ts provides a global Cache
import { CacheModule as NestCacheModule, Global, Module } from '@nestjs/common'
import { RedisConfigServer } from './redis.config.server';
import { RedisServer } from './redis.server';

/**
 * Redis
 * @export
 * @class RedisModule* /
@Global(a)@Module({
    imports: [
        NestCacheModule.registerAsync({
            useClass: RedisConfigServer,
            inject: [RedisConfigServer]
        })
    ],
    providers: [RedisConfigServer, RedisServer],
    exports: [RedisServer]
})

export class RedisModule {}Copy the code
  • Redis. Config. Server. Ts redis configuration
import { REDIS } from '@app/config'
import logger from '@app/utils/logger'
import { CacheModuleOptions, CacheOptionsFactory, Injectable } from '@nestjs/common'
import redisStore, { RedisStoreOptions } from './redis.store'

@Injectable(a)export class RedisConfigServer implements CacheOptionsFactory {
    // Retry policy
    private retryStrategy(retries: number) :number | Error {
        const errorMessage = ['[Redis]'.` retryStrategy! retries:${retries}`] logger.error(... (errorMessageas [any]))
        if (retries > 6) {
            return new Error('[Redis] number of attempts reached limit! ')}return Math.min(retries * 1000.3000)}public createCacheOptions(): CacheModuleOptions<Record<string.any>> | Promise<CacheModuleOptions<Record<string.any> > > {const redisOptions: RedisStoreOptions = {
            host: REDIS.host as string.port: REDIS.port as number.retry_strategy: this.retryStrategy.bind(this),}if (REDIS.password) {
            redisOptions.password = REDIS.password
        }
        return {
            isGlobal: true.store: redisStore,
            redisOptions,
        }
    }
}
Copy the code
  • Redis. Store. Ts redis connection
import { createClient, ClientOpts } from 'redis'
import { CacheStoreFactory, CacheStoreSetOptions, CacheModuleOptions } from '@nestjs/common'

export type RedisStoreOptions = ClientOpts
export type RedisCacheStore = ReturnType<typeof createRedisStore>

export interface CacheStoreOptions extends CacheModuleOptions {
    redisOptions: RedisStoreOptions
}

const createRedisStore = (options: CacheStoreOptions) = > {
    const client = createClient(options.redisOptions) as any

    const set = async <T>(key: string.value: T, options: CacheStoreSetOptions<T> = {}): Promise<void> = > {const { ttl } = options
        const _value = value ? JSON.stringify(value) : ' '
        if (ttl) {
            const _ttl = typeof ttl === 'function' ? ttl(value) : ttl
            await client.setEx(key, _ttl, _value)
        } else {
            await client.set(key, _value)
        }
    }

    const get = async <T>(key: string) :Promise<T> => {
        const value = await client.get(key)
        return value ? JSON.parse(value) : value
    }

    const del = async (key: string) = > {await client.del(key)
    }
    return { set, get, del, client }
}

const redisStoreFactory: CacheStoreFactory = {
    create: createRedisStore,
}

export default redisStoreFactory
Copy the code
  • Redis.server. ts provides services

import { CACHE_MANAGER, Inject, Injectable } from "@nestjs/common";
import { RedisCacheStore } from "./redis.store";
import { Cache } from 'cache-manager'
import logger from "@app/utils/logger";

@Injectable(a)export class RedisServer {
    publiccacheStore! : RedisCacheStoreprivate isReadied = false

    constructor(@Inject(CACHE_MANAGER) cacheManager: Cache) {
        this.cacheStore = cacheManager.store as RedisCacheStore
        this.cacheStore.client.on('connect'.() = > {
            logger.info('[Redis]'.'connecting... ')})this.cacheStore.client.on('reconnecting'.() = > {
            logger.warn('[Redis]'.'reconnecting... ')})this.cacheStore.client.on('ready'.() = > {
            this.isReadied = true
            logger.info('[Redis]'.'readied! ')})this.cacheStore.client.on('end'.() = > {
            this.isReadied = false
            logger.error('[Redis]'.'Client End! ')})this.cacheStore.client.on('error'.(error) = > {
            this.isReadied = false
            logger.error('[Redis]'.`Client Error! `, error.message)
        })
    }
}
Copy the code

The Session and guard

Sessions are stored in Redis, which allows sessions to be shared across multiple services. If the version is Redis4, the configuration is different.

import { Logger, MiddlewareConsumer, Module, NestModule } from '@nestjs/common';

// redis and session
import { RedisModule } from '@app/processors/redis/redis.module';
import { RedisServer } from '@app/processors/redis/redis.server';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { SESSION } from '@app/config';
import RedisStore from 'connect-redis';
import session from 'express-session';

// middlewares
import { CorsMiddleware } from '@app/middlewares/core.middleware';
import { OriginMiddleware } from '@app/middlewares/origin.middleware';

@Module({
    imports: [
        RedisModule,
    ],
    controllers: [AppController],
    providers: [AppService, Logger],
})
export class AppModule implements NestModule {
    private redis: any
    constructor(private readonly redisStore: RedisServer) {
        this.redis = this.redisStore.cacheStore.client
    }
    configure(consumer: MiddlewareConsumer) {
        consumer
            .apply(
                CorsMiddleware,
                OriginMiddleware,
                session({
                    store: new (RedisStore(session))({ client: this.redis }), ... SESSION }), ) .forRoutes(The '*'); }}Copy the code

Session has been configured. You can print request. Session to check whether the configuration is successful

The guards

API and route guard, and whitelist must be enabled to permit interfaces and routes that do not need authentication. Passport, currently the most popular Node.js authentication library, is used for authentication.

  • auth.module.ts
import { Module } from "@nestjs/common";
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt'
import jwt from 'jsonwebtoken'
import { AuthController } from "./auth.controller";
import { AuthService } from "./auth.service";
import { JwtStrategy } from "./jwt.strategy";
import { AUTH } from "@app/config";

@Module({
    imports: [
        PassportModule.register({ defaultStrategy: 'jwt' }),
        JwtModule.register({
            privateKey: AUTH.jwtTokenSecret as jwt.Secret,
            signOptions: {
                expiresIn: AUTH.expiresIn as number,}})],controllers: [AuthController],
    providers: [AuthService, JwtStrategy],
    exports: [AuthService]

})
export class AuthModule {}Copy the code
  • Auth.controller.ts provides a login interface, which is different from interface forwarding.
import { HttpRequest } from "@app/interfaces/request.interface";
import { TransformPipe } from "@app/pipes/transform.pipe";
import { Body, Controller, Get, Param, Post, Req, Res } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { Request, Response } from 'express'
import { ResponseStatus } from "@app/interfaces/response.interface";
import { Responsor } from "@app/decorators/responsor.decorator";

@Controller('api')
export class AuthController {

    constructor(private readonly authService: AuthService){}/** * Login interface *@param {Request} req
     * @param {HttpRequest} data
     * @param {Response} res
     * @return {*} 
     * @memberof AuthController* /
    @Responsor.api()
    @Post('login')
    public async adminLogin(@Req() req: Request, @Body(new TransformPipe()) data: HttpRequest, @Res() res: Response) {
        const{ access_token, token, ... result } =await this.authService.login(data)
        res.cookie('jwt', access_token);
        res.cookie('userId', result.userId);
        req.session.user = result;
        return res.status(200).send({
            result: result,
            status: ResponseStatus.Success,
            message: 'Login successful'})},/** * Run request * without guard@param {string} id
     * @return {*} 
     * @memberof AuthController* /
    @Get('user')
    @Responsor.api()
    public async getUserInfo(@Param('id') id: string) {
        return await this.authService.findById({ id })
    }
}
Copy the code
  • auth.service.ts
import { Injectable } from "@nestjs/common";
import { HttpRequest } from "@app/interfaces/request.interface";
import { AxiosService } from "@app/processors/axios/axios.service";
import { AUTH, config } from "@app/config";
import { JwtService } from '@nestjs/jwt'


@Injectable(a)export class AuthService {

    constructor(private readonly axiosService: AxiosService, private readonly jwtService: JwtService){}/** * Generate token *@param {*} data
     * @return {*} 
     * @memberof AuthService* /
    creatToken(data: any) {
        const token = {
            access_token: this.jwtService.sign({ data }),
            expires_in: AUTH.expiresIn as number,}return token
    }

    /** * Verify user *@param {*} { id }
     * @return {*} 
     * @memberof AuthService* /
    public async validateUser({ id, username }: any) {
        // Get the user
        const user = await this.findById(id);
        return user
    }

    /** * login *@param {HttpRequest} { transformUrl, transferData }
     * @return {*}  {Promise<any>}
     * @memberof AuthService* /
    public async login({ transformUrl, transferData }: HttpRequest): Promise<any> {
        const res = await this.axiosService.post(transformUrl, transferData) as any
        const token = this.creatToken({ usernmae: res.account, userId: res.userId })
        return{... res, ... token } }/** * Query user * by ID@param {*} id
     * @return {*}  {Promise<any>}
     * @memberof AuthService* /
    public async findById(id): Promise<any> {
        const url = config.apiPrefix.baseApi + '/user/info'
        const res = await this.axiosService.get(url, { params: { id: id } })
        return res
    }
}
Copy the code

Secure endpoints by requiring valid JWT at request time. Passports also help us. It provides a passport- JWT policy for securing RESTful endpoints with JSON Web tags. In the auth folder jwt.strategy.ts. The token is authenticated through a Cookie

import { AUTH } from "@app/config";
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from '@nestjs/passport'
import { Strategy } from 'passport-jwt'
import { AuthService } from "./auth.service";
import { Request } from 'express'

@Injectable(a)export class JwtStrategy extends PassportStrategy(Strategy) {
    constructor(private readonly authService: AuthService) {
        super({
            jwtFromRequest: (req: Request) = > {
                return req.cookies['jwt']},secretOrKey: AUTH.jwtTokenSecret
        })
    }

    async validate(payload: any) {
        const res = await this.authService.validateUser(payload);
        return res
    }
}
Copy the code

The API and route are guarded separately.

import { ExecutionContext, Injectable } from "@nestjs/common";
import { LoggedInGuard } from "./logged-in.guard";
import { HttpUnauthorizedError } from "@app/errors/unauthorized.error";
import { Request } from 'express'
import { ApiWhiteList } from "@app/constants/api.contant";

@Injectable(a)export class ApiGuard extends LoggedInGuard {
    private apiUrl: string
    canActivate(context: ExecutionContext) {
        const req = context.switchToHttp().getRequest<Request>()
        this.apiUrl = req.body.transformUrl || req.query.transformUrl
        return super.canActivate(context)
    }
    handleRequest(error, authInfo, errInfo) {
        const validToken = Boolean(authInfo)
        constemptyToken = ! authInfo && errInfo? .message ==='No auth token'
        if((! error && (validToken || emptyToken)) || ApiWhiteList.includes(this.apiUrl)) {
            return authInfo || {}
        } else {
            throw error || new HttpUnauthorizedError()
        }
    }
}
Copy the code
import { ExecutionContext, Injectable } from "@nestjs/common";
import { LoggedInGuard } from "./logged-in.guard";
import { HttpUnauthorizedError } from "@app/errors/unauthorized.error";
import { Request } from 'express'
import { RouterWhiteList } from "@app/constants/router.constant";
@Injectable(a)export class RouterGuard extends LoggedInGuard {
    private routeUrl: string
    canActivate(context: ExecutionContext) {
        const req = context.switchToHttp().getRequest<Request>()
        this.routeUrl = req.url
        return super.canActivate(context)
    }
    handleRequest(error, authInfo, errInfo) {
        if((authInfo && ! error && ! errInfo) || RouterWhiteList.includes(this.routeUrl)) {
            return authInfo
        } else {
            throw error || newHttpUnauthorizedError(errInfo? .message) } } }Copy the code

The usage is as follows:

/** * Post interface forward API guard *@param {HttpRequest} data
 * @return {*} 
 * @memberof ApiConstroller* /
@UseGuards(ApiGuard)
@Responsor.api()
@Post('transform')
postTransform(@Body(new TransformPipe()) data: HttpRequest) {
    return this.apiService.post(data)
}

/** * Render page route guard *@param {Request} req
 * @return {*} 
 * @memberof AppController* /
@UseGuards(RouterGuard)
@Get(a)@Render('index')
getTest(@Req() req: Request) {
    return { data: 12}}Copy the code

Intercepts interfaces and routes

import {
    Injectable,
    NestInterceptor,
    ExecutionContext,
    CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { Response, Request } from 'express'
import { HttpResponseSuccess, ResponseStatus } from '@app/interfaces/response.interface';
import { getResponsorOptions } from '@app/decorators/responsor.decorator';

/** * intercept *@export
 * @class TransformInterceptor
 * @implements {NestInterceptor<T, HttpResponse<T>>}
 * @template T* /
@Injectable(a)export class TransformInterceptor<T>
    implements NestInterceptor<T.T | HttpResponseSuccess<T>>
{
    intercept(context: ExecutionContext, next: CallHandler<T>): Observable<T | HttpResponseSuccess<T>> | any {
        const req = context.switchToHttp().getRequest<Request>();
        const res = context.switchToHttp().getResponse<Response>()
        const target = context.getHandler()
        const { isApi } = getResponsorOptions(target)
        
        // Refresh the session expiration time immediately
        req.session.touch();
        if(! isApi) { res.contentType('html')}return next.handle()
            .pipe(
                map((data: any) = > {
                    if (data.redirectUrl) return res.status(301).redirect(data.redirectUrl)
                    const result = isApi ? {
                        status: ResponseStatus.Success,
                        message: 'Request successful'.result: data,
                    } : ({ data })
                    returnresult }) ); }}Copy the code

Project running and attention

Project operation provides the following:

  • Redis services need to be provided
  • Server request interface
  • Login interface and user interface information interface must return the userId.

Run locally:

Access Node layer IP address

$ yarn start:dev
$ yarn dev
Copy the code

The last

At present, wechat scanning code login authorization has not been completed. After the subsequent COMPLETION of H5, the scanning code will be transplanted. Code address.

Please point out any mistakes. Optimization iterations were continued.