Nest + TypeScript + TypeOrm + JWT

Preface: I personally feel that JavaScript’s greatest strength is flexibility, and its greatest weakness is also flexibility. The development speed is fast, but the debugging and maintenance of the time will be much more than the strong type of language to spend a lot of time, running times is wrong, I think it as a back-end language is a big problem, the development of cross-file call IDE function and variable prompt, and type limit is ALSO I think some of the DEVELOPMENT problems of JS. These issues are well addressed in Typescript, plus the object-oriented stuff can be implemented in TS, and the basic stuff can be done in Node.

As the current technology stack of the company is JS, the back-end server development frameworks used in Node. js are egg, Nest, KOA, Express and so on.

In the previous project, the company used Egg and also studied some methods of ts. However, since there are many problems before the project, we are ready to refactor the previous code. Yes, I am the one who is firmly pushing TS.

Egg’s support for TS is not very good. Ali has Midway on the basis of Egg’s support for TS. Personally, I wrote a demo that I don’t think is very good. So I gave up the egg.

Select the TS framework in Node and select Nest.js. Nest is a better example.

Advantages of Nest:
  • Nest is similar to Spring Boot in Java, absorbing many excellent ideas and ideas. If you want to learn Spring Boot, you can start from this. For this kind of back end over the full stack is relatively easy to get started.
  • Egg Star (so far) : 15.7K, while Nest has 28.1K
  • Egg has it, Nest basically has it.
  • Nest supports aspect very well for object and aspect.
  • Dependency injection containers (also midway)
Disadvantages of Nest:
  • Not many people use it in China, but I found a lot of people do it in China.

Good nonsense, do not say, on the teaching address: github.com/liangwei010…

The life cycle

  1. When an Http request comes in from the client, the middleware goes through first.
  2. Then there are the past guards (guards only pass and don’t pass).
  3. Interceptor (here we can see that we can do certain things before and after the function is executed, uniform return format, etc.).
  4. Pipeline, we can do parameter checksum value conversion.
  5. And then finally it goes to the Controller, and then it returns the data to the client.

So here’s the directory structure for my project, but you don’t have to do that. Only parts of the hierarchy are listed, see the code for details.

Project ├ ─ ─ the SRC (all ts source here) │ ├ ─ ─ common (general category) │ │ └ ─ ─ the class (a collection of generic class) │ │ │ └ ─ ─ XXX. Ts watch business with (the) │ │ └ ─ ─ │ │ ├ ─ 0├ ─ ├ ─ garbage, ├ ─ garbage, ├ ─ imp GlobalGuard guards (global) │ │ │ └ ─ ─ apiErrorCode. Ts (API error collection) │ │ └ ─ ─ httpHandle (Http) │ │ │ └ ─ ─ httpException. Ts (Http abnormal unified process) │ │ └ ─ ─ interceptor (processing) interceptor │ │ │ └ ─ ─ httpException. Ts (HTTP abnormal unified handling) │ │ └ ─ ─ interface sets (interface) │ │ │ └ ─ ─ XXX. Ts (generic interface) │ │ └ ─ ─ middleware (middleware) │ │ │ └ ─ ─ logger. The middleware. The ts (log) middleware │ │ └ ─ ─ pipe (pipe) │ │ │ └ ─ ─ validationPipe. Ts (pipe validation global Settings) │ │ └ ─ ─ │ ├ ─ ├ ─ imp (sci-tech), ├ ─ sci-imp (sci-tech), sci-imp (sci-tech), sci-imp (sci-tech), sci-imp (sci-tech), sci-imp (sci-tech), sci-imp (sci-tech), sci-imp (sci-tech) Database (database module) │ │ └ ─ ─ utils directory (tools) │ │ │ └ ─ ─ stringUtil. Ts (string tool set) │ ├ ─ ─ the config (configuration file collection) │ │ └ ─ ─ dev (dev) │ │ │ └ ─ ─ Database (database configuration) │ │ │ └ ─ ─ development. Ts introduced (configuration) │ │ └ ─ ─ prod (prod) │ │ │ └ ─ ─ (ditto) │ │ └ ─ ─ the staging (staging configuration) │ │ │ └ ─ ─ (ditto) │ │ └ ─ ─ unitTest (unitTest configuration) │ │ │ └ ─ ─ (ditto) │ ├ ─ ─ the entity sets (database tables) │ │ └ ─ ─ the user. The entity. The ts (user) │ ├ ─ ─ modules (a collection of modules) │ │ └ ─ ─ the user (the user module) │ │ │ └ ─ ─ the user. The controller. The ts (controller) │ │ │ └ ─ ─ the user. The module. The ts (module declaration) │ │ │ └ ─ ─ User. Service. Ts (service) │ │ │ └ ─ ─ the user. The service. The spec. The ts (service) │ │ │ └ ─ ─ userDto. Ts (user Dto validation) │ ├ ─ ─ app. The module. The ts │ ├─ ├─ ├─ ├.json ├─ ├.json ├─ ├.txtCopy the code

The Controller layer

A Controller is the same as a regular Spring Boot Controller or egg or whatever. The request layer on the receiving front end. ** Suggestion: ** Services should not be placed in the Controller layer, but in the Service layer. If the service file is too large, you can split the file in namespace mode.

@controller () // This is a Controller layer export class UserController {// this is equivalent to new userService(), but the container will help you handle some dependencies. Here's learning the idea of Spring constructor(private readonly userService: UserService) {} @get () getHello(@body () createCatDto: createCatDto); string { console.log(createCatDto) return this.appService.getHello(); }}Copy the code

The Service layer

I deleted the default.spec.ts test file for the Controller layer because my unit tests were in xx.service.spec.ts.

@injectable () export class UserService {// This is a Repository of data User table operations. Constructor (@injectrepository (User) private usersRepository: Repository<User>) {} /** * createUser */ async createUser() {const User = new User(); user.userSource = '123456'; user.paymentPassword = '123'; Nickname = 'booty '; User.verifiedname = 'verifiedName '; const res = await this.usersRepository.save(user); return res; }}Copy the code

Service unit Testing

  • There are two types of unit tests: one is a test to connect to a database, and the other is a mock data test to see if the logic is correct. Here’s the mock first.
Const user = {"id": "2020-0620-1525-45106", "createTime": "2020-06-20T07:25:45.116z ", "updateTime": 1420-06-20T07:25:45.116z, phone: 18770919134, "locked": false, "role": "300", "nickname": "verifiedName": } describe('user.service', () => {let service: UserService; let repo: Repository<User>; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [UserService, {provide: getRepositoryToken(User), useValue: {// Mock out the data function involved in the database CURD create: jest.fn().mockResolvedValue(user), save: jest.fn().mockResolvedValue(user), findOne: jest.fn().mockResolvedValue(user), }, }, ], }).compile(); service = module.get<UserService>(UserService); repo = module.get<Repository<User>>(getRepositoryToken(User)); }); It ('createUser', async () => {const user = await service.createuser (); expect(user.phone).toEqual('18770919134'); }); }Copy the code

Here is a foreign big guy to write the test, still pretty full, there is a need to see: github.com/Zhao-Null/n…

DTO (Database Transfer Object)

This is not a unique term in Java. DTO is a database transfer object, so when we transmit data from the front end, we need to convert the checksum into the corresponding value of the database table, and then save. Here is the NEST DTO. Before the Controller processes it, we need to verify that the parameters are correct. For example, if we need a parameter and the front-end does not pass it, or the type of the pass is not correct.

@Injectable() export Class ValidationPipeConfig 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) { const errorMessageList = [] const errorsObj = errors[0].constraints for (const key in errorsObj) { if (errorsObj.hasOwnProperty(key)) { errorMessageList.push(errorsObj[key]) } } throw new CustomException(errorMessageList, HttpStatus.BAD_REQUEST); } return value; } private toValidate(metatype: any): boolean { const types = [String, Boolean, Number, Array, Object]; return ! types.find((type) => metatype === type); UseGlobalPipes (new ValidationPipeConfig());Copy the code
Dto export class CreateUserDto {@isnotempty ({message: 'Account is null'}) @isString ({message: 'account is null'}) 'account is to require' }) account: string; @IsNotEmpty({ message: 'name is null' }) @IsString({ message: 'name is not null and is a string' }) name: string; }Copy the code
@post ('/dto') async createTest(@body () createUserDto: CreateUserDto) { console.log(createUserDto) return true; }Copy the code

[” Account is null”, “account is to require”] will be returned if the parameter passed in front of the account field is null, or if the type is incorrect. This prevents making too many judgments to the business layer and reduces things. Of course, there is support for conversion, such as the string “1” to the number 1, see docs.nestjs.com/pipes for details

Global timeout

Set the global timeout period. If a request exceeds a specified timeout period, a timeout will be returned.

/ / / / the main ts global app using timeout intercept. UseGlobalInterceptors (new TimeoutInterceptor ());Copy the code
/** * You want to handle timeout for route requests. When your endpoint returns nothing after a period of time, * you want to terminate with an error response. * 10s timeout */ @injectable () export class TimeoutInterceptor implements NestInterceptor {public intercept(context: ExecutionContext, next: CallHandler): Observable<any> { return next.handle().pipe(timeout(10000)); }}Copy the code

Global success return format

Unified return format to facilitate unified processing of data and errors.

import { Injectable, NestInterceptor, CallHandler, ExecutionContext } from '@nestjs/common'; import { map, switchMap } from 'rxjs/operators'; import { Observable } from 'rxjs'; interface Response<T> { data: T; } /** * encapsulates the correct return format * {* data, * code: 200, * message: 'success' * } */ @Injectable() export class TransformInterceptor<T> implements NestInterceptor<T, Response<T>> { intercept(context: ExecutionContext, next: CallHandler<T>): Observable<Response<T>> { return next.handle().pipe( map(data => { return { data, code: 200, message: 'success', }; })); }}Copy the code

The format of the global success exception

There are custom exceptions and other exceptions. Custom exceptions will return the status code and system of the custom exception. And other exceptions will return exceptions and errors that the system will return.

import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common'; import { CustomException } from './customException'; @Catch(HttpException) export class HttpExceptionFilter implements ExceptionFilter { catch(exception: any, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse(); const request = ctx.getRequest(); let errorResponse: any; const date = new Date().toLocaleDateString() + ' ' + new Date().toLocaleTimeString(); If (exception instanceof CustomException) {// CustomException errorResponse = {code: Exception. GetErrorCode (), / / error code errorMessage: exception. GetErrorMessage (), the message: "error", url: Request. OriginalUrl, // Error url: date: date,}; } else {// Non-custom exception errorResponse = {code: exception.getStatus(), // Error code errorMessage: exception.message, url: Request. OriginalUrl, // Error url: date: date,}; } const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR; Response.status (status); response.status(status); response.status(status); response.header('Content-Type', 'application/json; charset=utf-8'); response.send(errorResponse); }}Copy the code

JWT encapsulation

In the JWT example, @useGuards (AuthGuard()) annotations are added to each function if interface verification is required. However, most interfaces require interface verification. So I chose to encapsulate one myself.

Here I have two ways to write, if there is suitable for their own, please choose.

  • Method 1: Wrap an annotation yourself.

Here is the name of our overridden local validation class, inherited from AuthGuard

///auth.local.guard.ts import { Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export Class Extends AuthGuard('local') {}Copy the code

Here is the name of our JWT validation class, inherited from AuthGuard

///jwt.auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') { }
Copy the code
/// jwt.strategy.ts @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { constructor() { super({ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), ignoreExpiration: false, secretOrKey: jwtConstants.secret, }); } async validate(payload: any) { return { userId: payload.account, password: payload.password }; }}Copy the code

There’s a custom exception that’s thrown, that’s written up there.

/// local.strategy.ts import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { Strategy } from 'passport-local'; import { AuthService } from '.. /auth.service'; import { CustomException } from '.. /.. /.. /httpHandle/customException'; import { ApiError } from '.. /.. /.. /enum/apiErrorCode'; /** * local authentication */ @injectable () export class LocalStrategy extends PassportStrategy(Strategy) {** * The constructor here passes the parameters necessary for Authorization to the parent class. When instantiated, the parent class is informed that the client request must use Authorization as the request header, and the header content prefix must be Bearer. When decoding the Authorization token, Use the secretKey secretOrKey: 'secretKey' to decode the authorization token as the payload at the time the token is created. */ constructor(private readonly authService: AuthService) { super({ usernameField: 'account', passwordField: 'password' }); } /** * validate implements the abstract method of the parent class. After decrypting the authorization token successfully, that is, the request authorization token is not expired, * will pass the decrypted payload as a parameter to the validate method. This method needs to do specific authorization logic. For example, here I used the user name to find out if the user exists. * If the user does not exist, the token is incorrect and may have been forged. In this case, throw an UnauthorizedException. * When a user exists, the user object is added to the REQ, and in subsequent REQ objects, you can use req.user to get the current logged-in user. */ async validate(account: string, password: string): Promise<any> { let user = await this.authService.validateUserAccount(account); if (! user) { throw new CustomException( ApiError.USER_IS_NOT_EXIST, ApiError.USER_IS_NOT_EXIST_CODE, ); } user = await this.authService.validateUserAccountAndPasswd(account, password); if (! user) { throw new CustomException( ApiError.USER_PASSWD_IS_ERROR, ApiError.USER_PASSWD_IS_ERROR_CODE, ); } return user; }}Copy the code

Global guard, the core of this is, when we go to implement, see if there is a no-auth annotation, if there is, just skip the default JWT and custom (login) validation. Of course, we also write the relevant whitelist here. Look at the notes first.

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; import { Observable } from 'rxjs'; import { Reflector } from '@nestjs/core'; import { IAuthGuard } from '@nestjs/passport'; import { JwtAuthGuard } from '.. /specialModules/auth/guards/jwt.auth.guard'; import { LocalAuthGuard } from '.. /specialModules/auth/guards/auth.local.guard'; @Injectable() export class GlobalAuthGuard implements CanActivate { constructor(private readonly reflector: Reflector) { } canActivate(context: ExecutionContext): Boolean | Promise < Boolean > | observables < Boolean > {/ / login for annotations const loginAuth = this.reflector.get<boolean>('login-auth', context.getHandler()); Const noAuth = this.reflector. Get < Boolean >('no-auth', context.gethandler ()); if (noAuth) { return true; } const guard = GlobalAuthGuard.getAuthGuard(loginAuth); // Execute the canActivate method of the selected Guard. CanActivate (context); } private static getAuthGuard(loginAuth: Boolean): private static getAuthGuard(loginAuth: Boolean) IAuthGuard { if (loginAuth) { return new LocalAuthGuard(); } else { return new JwtAuthGuard(); }}}Copy the code

@noauth () does not perform any checks, other interfaces default to JwtAuthGuard and LocalAuthGuard checks

// Custom decorator /** * login authentication */ export const LoginAuth = () => SetMetadata('login-auth', true);Copy the code
/// user.controller.ts @Get() @NoAuth() @ApiOperation({ description: 'get userList'}) async userList(@paginationdto) IPagination) { return await this.userService.getUserList(paginationDto); }Copy the code
  • Option 2: Add a whitelist to the configuration and check on the guard. This code will not write it, not complex, just play with it.

This is where the basic Resetful interface and business logic come into play. Next time, I’ll look at queues, GraphQL, and other business development stuff. See you next time.