DEV Community

qiuzhi99
qiuzhi99

Posted on

Introduction to BFF and NestJS

my github page: https://github.com/hfpp2012

Recently, our back-end partners have adopted the microservice architecture and split many domain services. As a big front-end, we must also make changes. Usually, a list requires an interface to get data, however, the microservice architecture requires a layer of n interfaces specifically for the front-end aggregation microservice architecture to facilitate front-end calls. Therefore, we have adopted the currently popular BFF method.

bff has no strong binding relationship with node, but it is too expensive to let front-end personnel familiarize themselves with backend language learning other than node. Therefore, we use node as the middle layer on the technology stack, the http Framework of the node uses nestjs.

BFF function

BFF(Backends For Frontends) is the backend that serves the front end. After the baptism of several projects, I have some insights into it. I think it mainly has the following functions:

  • API aggregation and pass-through: As mentioned above, multiple interfaces are aggregated to facilitate front-end call.
  • Interface Data formatting: The frontend page is only responsible for UI rendering and interaction, and does not handle complex data relationships. The readability and maintainability of the frontend code are improved.
  • Reduce personnel coordination costs: after the back-end microservices and large front-end bff are implemented and perfected, some of the later requirements only need to be developed by front-end personnel.

Scenario

Although BFF is popular, it cannot be used for popularity. It can only be used when it meets certain scenarios and the infrastructure is perfect. Otherwise, it will only increase project maintenance costs and risks, however, the profit is very small. I think the applicable scenarios are as follows:

  • The backend has stable domain services and requires an aggregation layer.
  • Requirements change frequently, and interfaces often need to change: the backend has a set of stable domain services for multiple projects, and the cost of changes is high, while the bff layer is for a single project, changes at the bff layer can achieve minimal cost changes.
  • Complete infrastructure: logs, links, server monitoring, performance monitoring, etc. (required)

Nestjs

I will introduce Nestjs from the perspective of a pure frontend entry-level backend Xiaobai.

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

What does the backend do after the front-end initiates a request?

First, we initiate a GET request.

fetch('/api/user')
    .then(res => res.json())
    .then((res) => {
        // do some thing
    })
Enter fullscreen mode Exit fullscreen mode

Assume that the nginx proxy has been configured (all requests starting with/api are sent to our bff service), and the backend will receive our requests, then the problem arises, what is it received through?

First, initialize a Nestjs project and create a user directory. The directory structure is as follows:

├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── main.ts
└── src
    ├── user
            ├── user.controller.ts
            ├── user.service.ts
            ├── user.module.ts
Enter fullscreen mode Exit fullscreen mode

Nestjs receives requests through routing at the Controller layer. Its code is as follows:

user.controller.ts

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

@Controller('user')
export class CatsController {
  @Get()
  findAll(@Req() request) {
    return [];
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, let's explain some basic knowledge of Nestjs. Using Nestjs to complete a basic service requires three parts: Module,Controller and Provider.

  • Module,It literally means a Module. The class modified by @ Module() in nestjs is a Module. In a specific project, we will use it as the entrance to the current sub-Module, for example, a complete project may have user modules, Commodity Management modules, personnel management modules, and so on.
  • Controller,It literally means a Controller, which is responsible for processing incoming requests from the client and responses returned by the server. The official definition is a class modified by @ Controller().

  • Provider,The literal meaning is a provider, which actually provides services for the Controller. The official definition is a class modified by @ Injectable(). Let me explain briefly: the preceding code directly processes the business logic at the Controller layer. With the subsequent business iteration, the requirements become more and more complex. This code is difficult to maintain. Therefore, you need to process the business logic at one layer, and the Provider is at this layer, it needs to be modified by @ Injectable().

Let's improve the above code, add a Provider, and create user.service.ts under the current module.

user.service.ts

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

@Injectable()
export class UserService {
    async findAll(req) {
        return [];
    }
}
Enter fullscreen mode Exit fullscreen mode

Then our Controller needs to make some changes

user.controller.ts

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

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

  @Get()
  findAll(@Req() request) {
    return this.userService.findAll(request);
  }
}
Enter fullscreen mode Exit fullscreen mode

In this way, our Controller and Provider are completed. The two layers perform their own duties and the code maintainability is enhanced.
Next, we need to inject the Controller and Provider into the Module. We create a new user.mo dule.ts file and write the following content:

user.module.ts

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

@Module({
    controllers: [UserController],
    providers: [UserService]
})
export default class UserModule {}
Enter fullscreen mode Exit fullscreen mode

In this way, one of our business modules is completed, and only user.mo dule.ts is introduced into the general module of the project. After the project is started, you can obtain the data by accessing '/api/user'. The code is as follows:

app.module.ts

import {Module} from '@nestjs/common';
import {AppController} from './app.controller';
import {AppService} from './app.service';
import UserModule from './modules/user/user.module';

@Module({
  imports: [UserModule],
  controllers: [AppController],
  providers: [AppService]
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Nestjs Common Modules

Through reading the above, we have learned how the process of running a service and the nestjs interface correspond to data, but there are still many details that have not been mentioned, such as a large number of decorators (@ Get,@ Req, etc.), the following will explain the commonly used modules of Nestjs

  • Basic features
    • Controller Controller
    • Provider (business logic)
    • Module a complete business Module
    • NestFactory creates a factory class for a Nest application.
  • Advanced features
    • Middleware Middleware
    • Exception Filter Exception Filter
    • Pipe Pipe
    • Guard Guard
    • Interceptor Interceptor

Controller, Provider, and Module have been mentioned above, so we will not explain them again here. NestFactory is actually a factory function used to create a Nestjs application, which is usually created in the portal file, this is the main.ts, 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();
Enter fullscreen mode Exit fullscreen mode

Decorator

Decorator is a common function in Nestjs. It provides some decorators for common request bodies. We can also customize decorators, you can easily use it wherever you want.

Alt Text

In addition to the above, there are also some decorators that modify the internal methods of the class. The most common ones are @ Get(),@ Post(),@ Put(),@ Delete(), etc, I believe that most frontend users can understand the meaning of these methods that are used to modify the interior of Contollor, so they will not explain them any more.

Middleware

Nestjs is a secondary encapsulation of Express. The middleware in Nestjs is equivalent to the middleware in Express. The most common scenarios are global logs, cross-domain, error handling, for common api service scenarios such as cookie formatting, the official explanation is as follows:

Middleware functions can access the request object (req), Response object (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.

Take cookie formatting as an example. The modified code of main.ts 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);

  app.use(cookieParser());
  await app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Exception Filter

Alt Text

Nestjs has a built-in exception layer that handles all thrown exceptions in the entire application. When an unhandled exception is captured, the end user will receive a friendly response.

As a frontend, we must have received an interface error. The exception filter is responsible for throwing an error. Usually, our project needs to customize the error format and form a certain Interface Specification after reaching an agreement with the frontend. The built-in exception filter provides the following format:

{
  "statusCode": 500,
  "message": "Internal server error"
}
Enter fullscreen mode Exit fullscreen mode

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 first implement a simple exception filter:

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

├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── common
 ├── filters
 ├── pipes
 ├── guards
 ├── interceptors
├── main.ts
└── src
    ├── user
            ├── user.controller.ts
            ├── user.service.ts
            ├── user.module.ts
Enter fullscreen mode Exit fullscreen mode

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 {Request, Response} from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response
      .status(status)
      .json({
        statusCode: status,
        timestamp: new Date().toISOString(),
        path: request.url,
      });
  }
}
Enter fullscreen mode Exit fullscreen mode

Next, we bind to the global and change our app.mo dule.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.ts'
import UserModule from './modules/user/user.module';

@Module({
  imports: [UserModule],
  controllers: [AppController],
  providers: [
      {
        provide: APP_FILTER,
        useClass: HttpExceptionFilter,
      },
      AppService
  ]
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

In this way, the initialized project has custom exception handling.

Pipe

Alt Text

This part is difficult to understand only in terms of name, but it is easy to understand in terms of function and application scenario. According to my understanding, pipelines are some processing programs for request data before Controllor processes them.

Generally, pipelines have two application scenarios:

  • Request data conversion
  • Request data verification: verifies the input data. If the verification succeeds, an exception is thrown.

There are not many scenarios for data conversion applications. Here are only examples of data verification. Data verification is the most common scenario for middle-end and back-end management projects.

Generally, our Nest application will cooperate with class-validator for data validation. We 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';

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  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);
  }
}
Enter fullscreen mode Exit fullscreen mode

Then we bind this pipeline globally. The modified app.mo dule.ts content is as follows:

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.ts'
import ValidationPipe from './common/pipes/validation.pipe.ts'
import UserModule from './modules/user/user.module';

@Module({
  imports: [UserModule],
  controllers: [AppController],
  providers: [
      {
        provide: APP_FILTER,
        useClass: HttpExceptionFilter,
      },
      {
        provide: APP_PIPE,
        useClass: ValidationPipe,
      },
      AppService
  ]
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

In this way, the data verification function is added to our application. For example, to write an interface that requires data verification, we need to create a new createUser.dto.ts file, which reads as follows:

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

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

  @IsInt()
  age: number;

  @IsString()
  breed: string;
}
Enter fullscreen mode Exit fullscreen mode

Then we introduce it at the Controller layer. The code is as follows:

user.controller.ts

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

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

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

  @Post()
  addUser(@Body() body: DTO.CreateUserDto) {
    return this.userService.add(body);
  }
}
Enter fullscreen mode Exit fullscreen mode

If the parameters passed by the client do not conform to the specifications, the request directly throws an error and will not continue processing.

Guard

Alt Text

Guard is actually a route guard, which protects the interfaces we write. The most common scenario is interface authentication. Generally, we have login authentication for each interface of a business system, therefore, we usually encapsulate a global route guard. We create auth in the common/guards Directory of the project. guard.ts, the code is as follows:

auth.guard.ts

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

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();

    return validateRequest(request);
  }
}
复制代码
Enter fullscreen mode Exit fullscreen mode

Then we bind it to the global module. The modified app.mo dule.ts content is as follows:

import {Module} from '@nestjs/common';
import {APP_GUARD, 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 RolesGuard from './common/guards/auth.guard'
import UserModule from './modules/user/user.module';

@Module({
  imports: [UserModule],
  controllers: [AppController],
  providers: [

      {
        provide: APP_FILTER,
        useClass: HttpExceptionFilter,
      },

      {
        provide: APP_PIPE,
        useClass: ValidationPipe,
      },

      {
        provide: APP_GUARD,
        useClass: RolesGuard,
      },
      AppService
  ]
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

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

Interceptor

Alt Text

As can be seen from the official figure, interceptors can intercept requests and responses, so they are divided into request interceptors and response interceptors. Currently, many popular front-end request libraries also have this function, such as axios,umi-request, etc. I believe that front-end employees have contacted it. It is actually a program that processes data between the client and the route.

Interceptor has a series of useful functions, which can:

  • Bind additional logic before or after function execution
  • Convert the result returned from the function
  • Convert the Exception thrown from the function
  • Extended BASIC function behavior
  • Completely rewrite the function based on the selected conditions (for example, cache purpose)

Next, we implement a response interceptor to format the global response data and create a new res.int erceptors.ts file in the/common/interceptors Directory. The content is as follows:

res.interceptors.ts

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

export interface Response<T> {
    code: number;
    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(data => {
            const ctx = context.switchToHttp();
            const request = ctx.getRequest();
            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};
    }
}
Enter fullscreen mode Exit fullscreen mode

The function of this response guard is to format the data returned by our interface into {code, data} format. Next, we need to bind this guard to the global, modified app.mo dule. The ts content is as follows:

import {Module} from '@nestjs/common';
import {APP_INTERCEPTOR, APP_GUARD, 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 RolesGuard from './common/guards/auth.guard';
import ResInterceptor from './common/interceptors/res.interceptor';
import UserModule from './modules/user/user.module';

@Module({
  imports: [UserModule],
  controllers: [AppController],
  providers: [

      {
        provide: APP_FILTER,
        useClass: HttpExceptionFilter,
      },

      {
        provide: APP_PIPE,
        useClass: ValidationPipe,
      },

      {
        provide: APP_GUARD,
        useClass: RolesGuard,
      },

      {
        provide: APP_INTERCEPTOR,
        useClass: ResInterceptor,
      },
      AppService
  ]
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

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

Nestjs summary

After a series of steps above, we have built a small application (without logs and data sources), then the problem arises, how does the application process and respond to data step by step after the frontend initiates a request? The procedure is as follows:

Client request-> Middleware Middleware-> Guard -> request interceptor (we don't have this)-> Pipe -> routing handler at Controllor layer-> response interceptor-> client response>

The routing processing function at the Controllor layer calls the Provider, which is responsible for obtaining the underlying data and processing the business logic.

Summary

Through the above, we can have a basic understanding of the concept of BFF layer, and we can build a small Nestjs application by ourselves according to the steps, but there is still a big gap with enterprise-level applications.
Enterprise applications also need to access essential functions such as data sources (backend interface data, database data, and apollo configuration data), logs, links, and caches.

  • To connect to the BFF layer, complete infrastructure and appropriate business scenarios are required. Do not access the BFF layer blindly.

  • Nestjs is implemented based on Express and refers to the design idea of springboot. It is easy to get started. You need to understand its principle, especially the idea of dependency injection.

my github page: https://github.com/hfpp2012

Discussion (0)