Introduction to BFF

Recently we back-end partners began to micro service architecture is adopted, break up a lot of field service, as the front of a big we must also want to make a change, usually a list need a interface can get the data, but the service framework will need to have a layer of specifically for the front-end polymerization under the micro service architecture of n interface, convenient front-end calls, So we adopted the current popular BFF method.

BFF does not have a strong binding relationship with Node, but it is too expensive for the front-end staff to be familiar with the back-end language learning beyond Node, so we use Node as the middle layer on the technical stack, and we use NestJS for the HTTP framework of Node.

BFF role

BFF (Backends For Frontends) is the Backends that serve the Frontends. After several projects, I have gained some insights about it, and I believe that it mainly serves the following functions:

  • Interface aggregation and pass-through: As mentioned above, multiple interfaces are aggregated to facilitate front-end invocation
  • Interface data formatting: The front-end page is only responsible for UI rendering and interaction, and does not deal with complex data relationships. The front-end code readability and maintainability will be improved
  • Reduce the cost of personnel coordination: after the implementation and improvement of back-end micro-service and large front-end BFF, part of the later needs only need front-end personnel development

Applicable scenario

Although BFF is popular, it should not be used for popularity. It should be used only when certain scenarios are met and the infrastructure is perfect, otherwise it will only increase project maintenance costs and risks, but the benefits are very small. I think the applicable scenarios are as follows:

  • There are stable domain services on the back end that require an aggregation layer
  • The demand changes frequently, and the interface often needs to change: the backend has a stable set of domain services for multiple projects, and the cost is high if the change, while the BFF layer is for a single project, and the change in the BFF layer can realize the minimal cost change.
  • Good infrastructure: logs, links, server monitoring, performance monitoring, etc. (prerequisite)

Nestjs

In this article, I will introduce Nestjs from the perspective of a pure front-end entry to the back-end.

Nest is a framework for building efficient, scalable Node.js server-side applications

What does the back end do after the front end initiates the request

First we make a GET request

fetch('/api/user')
    .then(res= > res.json())
    .then((res) = > {
    	// do some thing
    })
Copy the code

Assuming that nginx’s proxy is configured (all/AP-starting requests go to our BFF service), the back end will receive our request, then the question is, through what?

First we initialize a Nestjs project and create the User directory, which has the following directory structure

├ ─ ─ app. Controller. Ts# controller├ ─ ─ app. The module. Ts# root module├ ─ ─ app. Service. Ts# service├ ─ ─ the main ts# Project entry, you can choose platform, configure middleware, etc├── ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├── ├.class.class.class.class.class.classCopy the code

Nestjs receives requests via routing at the Controller layer, and its code looks like this:

user.controller.ts

import {Controller, Get, Req} from '@nestjs/common';

@Controller('user')
export class UserController {
  @Get(a)findAll(@Req() request) {
    return[]; }}Copy the code

Here to explain some basic knowledge of Nestjs use Nestjs needed to complete a basic service Module, Controller, three most of the Provider.

  • Module, which literally means module, in NestJS by@Module()The modified class is a Module, which we will use in our projectEntry to the current submoduleFor example, a complete project may have user modules, product management modules, personnel management modules and so on.
  • ControllerThe controller is responsible for handling incoming requests from clients and responses from servers@Controller()The code above is a Controller, when we initiate at address'/api/user'The Controller will locate the get requestfindAllThe return value of this method is the data received by the front end.
  • Provider“, literally means provider, which is actually a service to Controller. The official definition is defined by@Injectable()The code above does business logic processing directly in the Controller layer. As the business iterates, the requirements become more and more complex, and such code will be difficult to maintain.So you need a layer to handle the business logicProvider is the layer that needs it@Injectable()Modification.

Add Provider and create user.service.ts under the current module

user.service.ts

import {Injectable} from '@nestjs/common';

@Injectable(a)export class UserService {
    async findAll(req) {
        return[]; }}Copy the code

Then our Controller needs to be changed

user.controller.ts

import {Controller, Get, Req} from '@nestjs/common';
import {UserService} from './user.service';

@Controller('user')
export class UserController {
    constructor(
        private readonly userService: UserService
    ) {}

  @Get(a)findAll(@Req() request) {
        return this.userService.findAll(request); }}Copy the code

This completes our Controller and Provider, both layers doing their job, and the code becomes more maintainable.

Next, we need to inject the Controller and Provider into the Module. We create a new user.module.ts file and write the following:

user.module.ts

import {Module} from '@nestjs/common';
import {UserController} from './user.controller';
import {UserService} from './user.service';

@Module({
    controllers: [UserController],
    providers: [UserService]
})
export class UsersModule {}
Copy the code

This completes one of our business modules. All we need to do is add user.module.ts to the main module of the project and inject it. After starting the project, access ‘/ API /user’ to get the data.

app.module.ts

import {Module} from '@nestjs/common';
import {APP_FILTER} from '@nestjs/core';
import {AppController} from './app.controller';
import {AppService} from './app.service';
import {UsersModule} from './users/users.module';

@Module({
    // Import the business module
    imports: [UsersModule],
    controllers: [AppController],
    providers: [
        AppService
    ]
})
export class AppModule {}
Copy the code

Nestjs common modules

By reading the above we have learned the process of running a service and how the interface of NestJS is corresponding to the data, but there are many details that are not discussed, such as the use of a large number of decorators (@get, @req, etc.), the following will explain the common modules of NestJS

  • Basis function
    • The Controller Controller
    • Provider Provider (business logic)
    • Module A complete service Module
    • NestFactory creates the Factory class for the Nest application
  • Advanced features
    • Middleware Middleware
    • Exception Filter Indicates the Exception Filter
    • Pipe line
    • Guard the guards
    • Interceptor Interceptor

NestFactory is a factory function used to create a Nestjs application. It is usually created in the entry file (main.ts) in the directory above. The code is as follows:

main.ts

import {NestFactory} from '@nestjs/core';
import {AppModule} from './app.module';

async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    await app.listen(3000);
}
bootstrap();

Copy the code

The decorator decorator

Decorators are a common feature in Nestjs, it provides some common request body decorators inside, we can also customize decorators, you can easily use it wherever you want.

In addition to the above, there are other decorators that decorate the inner methods of a class. The most common ones are @get (), @post (), @PUT (), @delete () and other route decorators. I’m sure most of the front end will understand what these mean without explaining them.

Middleware Middleware

Nestjs is the secondary encapsulation of Express. Middleware in Nestjs is equivalent to middleware in Express. The most common scenarios are global logging, cross-domain, error handling, cookie formatting and other common API service application scenarios.

Middleware functions can access request objects (REQ), response objects (RES), and the next middleware function in the application’s request/response loop. The next middleware function is usually represented by a variable named next.

Taking cookie formatting as an example, the modified main.ts code is as follows:

import {NestFactory} from '@nestjs/core';
import * as cookieParser from 'cookie-parser';
import {AppModule} from './app.module';

async function bootstrap() {
    const app = await NestFactory.create(AppModule);
    // Cookie formatting middleware, after this middleware processing, we can get the cookie object in the REQ
    app.use(cookieParser());
    await app.listen(3000);
}
bootstrap();

Copy the code

Exception Filter Indicates the Exception Filter

Nestjs has a built-in exception layer that handles all exceptions thrown throughout the application. When an unhandled exception is caught, the end user receives a friendly response.

As the front end, we must have received an interface error, and the exception filter is responsible for throwing the error. Usually, our project needs to define the error format, and form a certain interface specification after reaching agreement with the front end. The built-in exception filter gives us the following format:

{
  "statusCode": 500."message": "Internal server error"
}
Copy the code

In general, this format does not meet our needs, so we need to customize the exception filter and bind it to the global. Let’s implement a simple exception filter first:

We added a common folder on the basis of this project, which stores some filters, guards, pipes, etc. The updated directory structure is as follows:

├ ─ ─ app. Controller. Ts# controller├ ─ ─ app. The module. Ts# root module├ ─ ─ app. Service. Ts# service├── Filters ├── Pipes ├─ Guards ├─ Interceptors ├─ Main# Project entry, you can choose platform, configure middleware, etc├── ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├─ ├── ├.class.class.class.class.class.classCopy the code

We add the http-exception.filter.ts file to the filters directory

http-exception.filter.ts

import {ExceptionFilter, Catch, ArgumentsHost, HttpException} from '@nestjs/common';
import {Response} from 'express';

// The Catch() modifier is required and ExceptionFilter is inherited
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
    // The filter requires a catch(exception: T, host: ArgumentsHost) method
    catch(exception: HttpException, host: ArgumentsHost) {
        const ctx = host.switchToHttp();
        const response = ctx.getResponse<Response>();
        const status = exception.getStatus();
        const msg = exception.message;

        // The handling of res is the format returned by the global error request
        response
            .status(status)
            .json({
                status: status,
                code: 1,
                msg,
                data: null}); }}Copy the code

Next we bind globally, and we change our app.module.ts again

app.module.ts

import {Module} from '@nestjs/common';
import {APP_FILTER} from '@nestjs/core';
import {AppController} from './app.controller';
import {AppService} from './app.service';
import {HttpExceptionFilter} from './common/filters/http-exception.filter';
import {UsersModule} from './users/users.module';

@Module({
    // Import the business module
    imports: [UsersModule],
    controllers: [AppController],
    providers: [
        // Global exception filter
        {
            provide: APP_FILTER,
            useClass: HttpExceptionFilter,
        },
        AppService
    ]
})
export class AppModule {}
Copy the code

This way our initialized project has custom exception handling.

Pipe line

This part is hard to understand from its name alone, but it’s easy to understand from its role and application scenarios. From my understanding, pipes are just handlers of the requested data before Controllor processes it.

There are two common application scenarios for pipes:

  • Request data conversion
  • Request data validation: Validates input data and continues delivery if validation succeeds; An exception is thrown if validation fails

There are few application scenarios for data transformation. Here we will only talk about the example of data verification, which is the most common scenario for middle and background management projects.

We will create Validation.pipe. ts in the Pipes directory

validation.pipe.ts

import {PipeTransform, Injectable, ArgumentMetadata, BadRequestException} from '@nestjs/common';
import {validate} from 'class-validator';
import {plainToClass} from 'class-transformer';

// Pipe requires @Injectable(); PipeTransform is optionally inherited from Nest's built-in pipe
@Injectable(a)export class ValidationPipe implements PipeTransform<any> {
    // The pipe must have a transform method, which takes two parameters, value: the parameter being processed, and metadata: the metadata
    async transform(value: any, {metatype}: ArgumentMetadata) {
        if(! metatype || !this.toValidate(metatype)) {
            return value;
        }
        const object = plainToClass(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);
    }
}

Copy the code

We then bind the pipe globally, and the modified app.module.ts will look like this:

import {Module} from '@nestjs/common';
import {APP_FILTER, APP_PIPE} from '@nestjs/core';
import {AppController} from './app.controller';
import {AppService} from './app.service';
import {HttpExceptionFilter} from './common/filters/http-exception.filter';
import {ValidationPipe} from './common/pipes/validation.pipe';
import {UsersModule} from './users/users.module';

@Module({
    // Import the business module
    imports: [UsersModule],
    controllers: [AppController],
    providers: [
        // Global exception filter
        {
            provide: APP_FILTER,
            useClass: HttpExceptionFilter,
        },
        // Global data format validation pipeline
        {
            provide: APP_PIPE,
            useClass: ValidationPipe,
        },
        AppService
    ]
})
export class AppModule {}
Copy the code

Create createUser.to.ts (createUser.to.ts, createUser.to.ts, createUser.to.ts, createUser.to.ts, createUser.to.ts);

import {IsString, IsInt} from 'class-validator';

export class CreateUserDto {
  @IsString(a)name: string;

  @IsInt(a)age: number;
}
Copy the code

Then we introduce it in the Controller layer as follows:

user.controller.ts

import {Controller, Get, Post, Req, Body} from '@nestjs/common';
import {UserService} from './user.service';
import * as DTO from './createUser.dto';


@Controller('user')
export class UserController {
    constructor(
        private readonly userService: UserService
    ) {}

    @Get(a)findAll(@Req() request) {
        return this.userService.findAll(request);
    }

    // Add data validation here
    @Post(a)addUser(@Body() body: DTO.CreateUserDto) {
        return this.userService.add(body); }}Copy the code

If the client passes a parameter that does not conform to the specification, the request is thrown incorrectly and will not be processed.

Guard the guards

The guard, in fact, is the route guard, is to protect the interface we write, the most common scenario is interface authentication, usually for a business system each interface we will have login authentication, so usually we will encapsulate a global route guard, We created auth.guard.ts in the common/ Guards directory of the project, with the following code:

auth.guard.ts

import {Injectable, CanActivate, ExecutionContext} from '@nestjs/common';
import {Observable} from 'rxjs';

function validateRequest(req) {
    return true;
}

// Guards need to be decorated with @Injectable() and inherit CanActivate
@Injectable(a)export class AuthGuard implements CanActivate {
    // The guard must have a canActivate method that returns a Boolean value
    canActivate(
        context: ExecutionContext,
    ): boolean | Promise<boolean> | Observable<boolean> {
        const request = context.switchToHttp().getRequest();
        // An authentication function that returns true or false
        returnvalidateRequest(request); }}Copy the code

Then we bind it to the global Module, and the modified app.module.ts looks like this:

import {Module} from '@nestjs/common';
import {APP_FILTER, APP_PIPE, APP_GUARD} from '@nestjs/core';
import {AppController} from './app.controller';
import {AppService} from './app.service';
import {HttpExceptionFilter} from './common/filters/http-exception.filter';
import {ValidationPipe} from './common/pipes/validation.pipe';
import {AuthGuard} from './common/guards/auth.guard';
import {UsersModule} from './users/users.module';

@Module({
    // Import the business module
    imports: [UsersModule],
    controllers: [AppController],
    providers: [
        // Global exception filter
        {
            provide: APP_FILTER,
            useClass: HttpExceptionFilter,
        },
        // Global data format validation pipeline
        {
            provide: APP_PIPE,
            useClass: ValidationPipe,
        },
        // Global login authentication guard
        {
            provide: APP_GUARD,
            useClass: AuthGuard,
        },
        AppService
    ]
})
export class AppModule {}
Copy the code

In this way, our application has the function of global guard

Interceptor Interceptor

It can be seen from the official figure that interceptors can intercept requests and responses, so they are divided into request interceptors and response interceptors. Front-end many popular request libraries also have this function, such as AXIos, UMi-Request and so on. I believe that front-end students have come into contact with it, which is actually a data processing program between the client and the route.

Interceptors have a range of useful functions that can:

  • Bind additional logic before/after function execution
  • Converts the result returned from the function
  • Converts an exception thrown from a function
  • Extend the basic function behavior
  • Completely rewrite the function based on the selected criteria (for example, caching purposes)

The /common/interceptors directory will provide a new res.interceptors.ts file with the following contents:

res.interceptors.ts

import {Injectable, NestInterceptor, ExecutionContext, CallHandler} from '@nestjs/common';
import {Observable} from 'rxjs';
import {map} from 'rxjs/operators';

export interface Response<T> {
    code: number;
    data: T;
}

@Injectable(a)export class ResInterceptor<T> implements NestInterceptor<T.Response<T>> {

    intercept(context: ExecutionContext, next: CallHandler): Observable<Response<T>> {
        return next.handle().pipe(map(data= > {
            const ctx = context.switchToHttp();
            const response = ctx.getResponse();
            response.status(200);
            const res = this.formatResponse(data) as any;
            return res;
        }));
    }

    formatResponse<T>(data: any): Response<T> {
        return {code: 0, data}; }}Copy the code

This response guard is used to format our interface data into {code, data}. Next we need to bind this guard globally. The modified app.module.ts content is as follows:

import {Module} from '@nestjs/common';
import {APP_FILTER, APP_PIPE, APP_GUARD, APP_INTERCEPTOR} from '@nestjs/core';
import {AppController} from './app.controller';
import {AppService} from './app.service';
import {HttpExceptionFilter} from './common/filters/http-exception.filter';
import {ValidationPipe} from './common/pipes/validation.pipe';
import {AuthGuard} from './common/guards/auth.guard';
import {ResInterceptor} from './common/interceptors/res.interceptors';
import {UsersModule} from './users/users.module';

@Module({
    // Import the business module
    imports: [UsersModule],
    controllers: [AppController],
    providers: [
        // Global exception filter
        {
            provide: APP_FILTER,
            useClass: HttpExceptionFilter,
        },
        // Global data format validation pipeline
        {
            provide: APP_PIPE,
            useClass: ValidationPipe,
        },
        // Global login authentication guard
        {
            provide: APP_GUARD,
            useClass: AuthGuard,
        },
        // Global response interceptor
        {
            provide: APP_INTERCEPTOR,
            useClass: ResInterceptor,
        },
        AppService
    ]
})
export class AppModule {}

Copy the code

In this way, the response format of all the interfaces in our application is fixed.

Small Nestjs summary

After a series of steps above, we have built a small application (no logs and data sources), so the question is, how does the application we implemented process and respond to the data step by step after the request from the front end? The steps are as follows:

Client request -> Middleware -> Guard -> Request interceptor (we don’t have one) -> Pipe Pipe -> Controllor layer routing handler -> Response interceptor -> client response

The Controllor routing function calls Provider, which obtains the underlying data and processes the business logic. Exception filters are executed after the program throws an error.

conclusion

After the above we can have a basic understanding of the concept of BFF layer, and according to the steps you can build a Small Nestjs application, but there is a big gap with enterprise applications.

Enterprise applications also need access to data sources (back-end interface data, database data, Apollo configuration data), logs, links, caching, monitoring and other essential functions.

  • Access to the BFF layer requires sound infrastructure and appropriate business scenarios, not blind access
  • Nestjs is based on Express and refers to springboot design ideas. It is easy to get started, but you need to understand its principles, especially the design ideas of dependency injection

reference

  • I understand BFF
  • NestJs official documentation