In Getting Started with Nestjs (2), we created a basic Nestjs application. Let’s expand on that.

Source address: awesome- Nest

serialization

In an Entity, sometimes some fields don’t have to be returned to the front end, and we usually need to filter them ourselves. Nestjs, with class-Transformer, can easily implement this function.

For example, if we have an entity base class common.entity.ts, and we don’t want to include create_at and update_at when returning data, we can use @exclude () to Exclude both fields from CommonEntity:

import { CreateDateColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
import { Exclude } from 'class-transformer'

export class CommonEntity {
  @PrimaryGeneratedColumn('uuid')
  id: string

  @Exclude(a)@CreateDateColumn({
    comment: 'Creation time',
  })
  create_at: number

  @Exclude(a)@UpdateDateColumn({
    comment: 'Update time',
  })
  update_at: number
}
Copy the code

Where to request use ClassSerializerInterceptor tag, at this point, the GET/API/v1 / cats / 1 the request returns data, will not contain create_at and update_at these two fields.

@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}@Get(':id')
  @UseInterceptors(ClassSerializerInterceptor)
  findOne(@Param('id') id: string) :Promise<Array<Partial<CatEntity>>> {
    return this.catsService.getCat(id)
  }
}
Copy the code

If you need to use in a controller ClassSerializerInterceptor to help us do some serialized work, can put the Interceptor ascend to the controller:

@UseInterceptors(ClassSerializerInterceptor)
@Controller('cats')
export class CatsController {
  constructor(private readonly catsService: CatsService) {}@Get(':id')
  findOne(@Param('id') id: string) :Promise<Array<Partial<CatEntity>>> {
    return this.catsService.getCat(id)
  }
  
  @Post()
  create(@Body() createCatDto: CreateCatDto): Promise<void> {
    return this.catsService.createCat(createCatDto)
  }
}
Copy the code

You can even make it a global Interceptor in main.ts, but that’s not easy for fine-grained control.

async function bootstrap() {
  const app = await NestFactory.create(AppModule)
  app.setGlobalPrefix('api/v1')

  app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)))

  await app.listen(config.port, config.hostName, (a)= > {
    Logger.log(
      `Awesome-nest API server has been started on http://${config.hostName}:${config.port}`,
    )
  })
}

bootstrap()

Copy the code

In situations where we need to return a field from an Entity, we can use @transform () :

@Entity('dog')
export class DogEntity extends CommonEntity {
  @Column({ length: 50 })
  @Transform(value= > `dog: ${value}`)
  name: string

  @Column()
  age: number

  @Column({ length: 100, nullable: true })
  breed: string
}
Copy the code

At this point, the name field is wrapped by @Transform to the dog: name format. If we need to construct a new field from an existing field, we can use @expose () :

@Entity('dog')
export class DogEntity extends CommonEntity {
  @Column({ length: 50 })
  @Transform(value= > `dog: ${value}`)
  name: string

  @Column()
  age: number

  @Column({ length: 100, nullable: true })
  breed: string

  @Expose(a)get isOld(): boolean {
    return this.age > 10}}Copy the code

The above code will dynamically calculate the value of isOld based on the queried age field, and the result returned by the GET method is as follows:

{
    "data": [{"id": "15149ec5-cddf-4981-89a0-62215b30ab81"."name": "dog: nana"."age": 12."breed": "corgi"."isOld": true}]."status": 0."message": "Request successful"
}
Copy the code

The transaction

When using MySQL, sometimes we need to use transactions. Using TypeORM, we can use transactions like this:

@Delete(':name')
@Transaction(a)delete(
  @Param('name') name: string.@TransactionManager() manager: EntityManager,
): Promise<void> {
    return this.catsService.deleteCat(name, manager)
}
Copy the code

@Transaction() wraps all the execution in a controller or service into a database Transaction, and @TransportManager provides a Transaction entity manager that must be used to execute queries in that Transaction:

async deleteCat(name: string, manager: EntityManager): Promise<void> {
  await manager.delete(CatEntity, { name })
}
Copy the code

The above code makes it easy to perform transactions with decorators, and automatically rolls back if there are any errors during the transaction execution.

Of course, we can also manually create an instance of the query runner and use it to manually control the transaction state:

import { getConnection } from "typeorm";

// Get the connection and create a new queryRunner
const connection = getConnection();
const queryRunner = connection.createQueryRunner();

// Use our new queryRunner to set up a real database connection
await queryRunner.connect();

// Now we can execute any query on the queryRunner, for example:
await queryRunner.query("SELECT * FROM users");

// We can also access the entity manager used with the connection created by queryRunner:
const users = await queryRunner.manager.find(User);

// Start business:
await queryRunner.startTransaction();

try {
  // Perform some operations on this transaction:
  await queryRunner.manager.save(user1);
  await queryRunner.manager.save(user2);
  await queryRunner.manager.save(photos);

  // Submit transaction:
  await queryRunner.commitTransaction();
} catch (err) {
  // There was an error to make a rollback change
  await queryRunner.rollbackTransaction();
}
Copy the code

QueryRunner provides a single database connection. Organize transactions using a query runner. A single transaction can only be set up on a single query runner.

certification

In this application, the user has not been authenticated, and the validity and permission of the access role can be determined through user authentication. Authentication is usually based on either Session or Token. Here is the Token based JWT (JSON Web Token) method for user authentication.

First install the dependencies:

$ npm install --save @nestjs/passport passport @nestjs/jwt passport-jwt
Copy the code

Then create jwt.Strategy.ts, which validates the token. If the token is valid, allow further processing of the request, otherwise return 401(Unanthorized) :

import { ExtractJwt, Strategy } from 'passport-jwt'
import { PassportStrategy } from '@nestjs/passport'
import { Injectable, UnauthorizedException } from '@nestjs/common'
import config from '.. /.. /config'
import { UserEntity } from '.. /entities/user.entity'
import { AuthService } from './auth.service'

@Injectable(a)export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor(private readonly authService: AuthService) {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: config.jwt.secret,
    })
  }

  async validate(payload: UserEntity) {
    const user = await this.authService.validateUser(payload)
    if(! user) {throw new UnauthorizedException('Authentication failed')}return user
  }
}
Copy the code

Then create auth.service.ts. The above jwt.Strategy. ts validates the token using this service and provides a method to create the token:

import { JwtService } from '@nestjs/jwt' import { Injectable } from '@nestjs/common' import { UserEntity } from '.. /entities/user.entity' import { InjectRepository } from '@nestjs/typeorm' import { Repository } from 'typeorm' import { Token } from './auth.interface' import config from '.. /.. /config' @Injectable() export class AuthService { constructor( @InjectRepository(UserEntity) private readonly userRepository: Repository<UserEntity>, private readonly jwtService: JwtService, ) { } createToken(email: string): Token { const accessToken = this.jwtService.sign({ email }) return { expires_in: config.jwt.signOptions.expiresIn, access_token: accessToken, } } async validateUser(payload: UserEntity): Promise<any> { return await this.userRepository.find({ email: payload.email }) } }Copy the code

Both files are registered as services in the corresponding Module, and both PassportModule and JwtModule are introduced:

import { Module } from '@nestjs/common'
import { AuthService } from './auth/auth.service'
import { PassportModule } from '@nestjs/passport'
import { JwtModule } from '@nestjs/jwt'
import { JwtStrategy } from './auth/jwt.strategy'
import config from '.. /config'


@Module({
  imports: [
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.register(config.jwt),
  ],
  providers: [
    AuthService,
    JwtStrategy,
  ],
  exports: [],
})
export class FeaturesModule {
}
Copy the code

In this case, @useGuards (AuthGuard()) can be used to authenticate apis that need authentication:

import {
  Body,
  ClassSerializerInterceptor,
  Controller,
  Get,
  Param,
  Post,
  UseGuards,
  UseInterceptors,
} from '@nestjs/common'

import { CatsService } from './cats.service'
import { CreateCatDto } from './cat.dto'
import { CatEntity } from '.. /entities/cat.entity'
import { AuthGuard } from '@nestjs/passport'

@Controller('cats')
@UseGuards(AuthGuard())
export class CatsController {
  constructor(private readonly catsService: CatsService) {}@Get(':id')
  @UseInterceptors(ClassSerializerInterceptor)
  findOne(@Param('id') id: string) :Promise<Array<Partial<CatEntity>>> {
    return this.catsService.getCat(id)
  }

  @Post()
  create(@Body() createCatDto: CreateCatDto): Promise<void> {
    return this.catsService.createCat(createCatDto)
  }
}
Copy the code

A Postman mock request without a token returns the following:

{
    "message": {
        "statusCode": 401."error": "Unauthorized"
    },
    "status": 1
}
Copy the code

security

There are two types of attacks in Web security: XSS (cross-site scripting) and CSRF (cross-site request forgery).

For JWT authentication, there is no COOKIE, so there is no CSRF. If you are not using JWT authentication, you can use the cSURF library to solve this security problem.

For XSS, you can use a helmet to take precautions. There are 12 middleware in a helmet that will set some security-related HTTP headers. For example, xssFilter is used to do some XSS related protection.

For violent attacks with a large number of requests from a single IP address, you can use express-rate-limit to limit the speed.

For common cross-domain problems, Nestjs provides two ways to solve them. One is to enable cross-domain using app.enablecors (), and the other is to enable it in the Nest option object as shown below.

Finally, all of these Settings are enabled as global middleware. Finally, in main.ts, the security-related Settings are as follows:

import * as helmet from 'helmet'
import * as rateLimit from 'express-rate-limit'

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { cors: true })

  app.use(helmet())
  app.use(
    rateLimit({
      windowMs: 15 * 60 * 1000.// 15 minutes
      max: 100.// limit each IP to 100 requests per windowMs}),)await app.listen(config.port, config.hostName, (a)= > {
    Logger.log(
      `Awesome-nest API server has been started on http://${config.hostName}:${config.port}`,)})}Copy the code

The HTTP request

Axios is encapsulated in Nestjs and built into the HttpModule as an HttpService. The HttpService returns the same type as the Angular HttpClient Module, which is observables, so you can use operators in RXJS to handle various asynchronous operations.

First, we need to import HttpModule:

import { Global, HttpModule, Module } from '@nestjs/common'

import { LunarCalendarService } from './services/lunar-calendar/lunar-calendar.service'

@Global(a)@Module({
  imports: [HttpModule],
  providers: [LunarCalendarService],
  exports: [HttpModule, LunarCalendarService],
})
export class SharedModule {}
Copy the code

Here we treat HttpModule as a global module and import and export it to sharedModule for use by other modules. In this case, we can use HttpService. For example, we can inject HttpService into LunarCalendarService and call its GET method to request the lunar calendar information for the day. Get returns an Observable.

The map operator in RXJS can be used to filter the data. After 5 seconds, a timeout error will be generated. CatchError will catch all errors. The returned value is converted to an Observable using the of operator:

import { HttpService, Injectable } from '@nestjs/common'
import { of, Observable } from 'rxjs'
import { catchError, map, timeout } from 'rxjs/operators'

@Injectable(a)export class LunarCalendarService {
  constructor(private readonly httpService: HttpService) {
  }

  getLunarCalendar(): Observable<any> {
    return this.httpService
      .get('https://www.sojson.com/open/api/lunar/json.shtml')
      .pipe(
        map(res= > res.data.data),
        timeout(5000),
        catchError(error= > of(`Bad Promise: ${error}`)))}}Copy the code

If you need to configure axios, you can set it directly at Module registration:

import { Global, HttpModule, Module } from '@nestjs/common'

import { LunarCalendarService } from './services/lunar-calendar/lunar-calendar.service'

@Global(a)@Module({
  imports: [
    HttpModule.register({
      timeout: 5000,
      maxRedirects: 5,
    }),
  ],
  providers: [LunarCalendarService],
  exports: [HttpModule, LunarCalendarService],
})
export class SharedModule {}
Copy the code

Template rendering

In Nestjs, you can use HBS as the template rendering engine:

$ npm install --save hbs
Copy the code

In main.ts, we tell express that the static folder is used to store static files, and the views contains the template file:

import { NestFactory } from '@nestjs/core'
import { NestExpressApplication } from '@nestjs/platform-express'
import { join } from 'path'

import { AppModule } from './app.module'
import config from './config'
import { Logger } from './shared/utils/logger'

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule, {
    cors: true,
  })

  app.setGlobalPrefix('api/v1')

  app.useStaticAssets(join(__dirname, '.. '.'static'))
  app.setBaseViewsDir(join(__dirname, '.. '.'views'))
  app.setViewEngine('hbs')

  await app.listen(config.port, config.hostName, (a)= > {
    Logger.log(
      `Awesome-nest API server has been started on http://${config.hostName}:${config.port}`,)})}Copy the code

Create a new file catspage.hbs under views and assume that the data structure we need to populate is like this:

{
  cats: [
    {
      id: 1,
      name: 'yyy',
      age: 12,
      breed: 'black cats'
    }
  ],
  title: 'Cats List',}Copy the code

In this case, you can write the template like this:


      
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0"/>
    <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
    <style>
        .table .default-td {
            width: 200px;
        }

        .table tbody>tr:nth-child(2n-1) {
            background-color: rgb(219, 212, 212);
        }

        .table tbody>tr:nth-child(2n) {
            background-color: rgb(172, 162, 162);
        }
    </style>
</head>
<body>
<p>{{ title }}</p>
<table class="table">
    <thead>
    <tr>
        <td class="id default-td">id</td>
        <td class="name default-td">name</td>
        <td class="age default-td">age</td>
        <td class="breed default-td">breed</td>
    </tr>
    </thead>
    <tbody>
    {{#each cats}}
        <tr>
            <td>{{id}}</td>
            <td>{{name}}</td>
            <td>{{age}}</td>
            <td>{{breed}}</td>
        </tr>
    {{/each}}
    </tbody>
</table>
</body>
</html>
Copy the code

It is important to note that if you have interceptors, the data will be processed by the interceptors before being filled into the template.

In controller, specify the name of the template with @render and return the data to be populated:

@Get('page')
@Render('catsPage')
getCatsPage() {
  return {
    cats: [
      {
        id: 1,
        name: 'yyy',
        age: 12,
        breed: 'black cats'
      }
    ],
    title: 'Cats List',}}Copy the code

Nestjs also supports integration with other SSR frameworks such as Next, Angular Universal, and Nuxt. You can use the Demo to view each of these projects separately: Nestify, Nest-Angular, and simple-todos.

Swagger document

Support for Swagger documentation is also available in Nestjs for tracking and testing the API:

$ npm install --save @nestjs/swagger swagger-ui-express
Copy the code

In main.ts the component document:

const options = new DocumentBuilder()
    .setTitle('Awesome-nest')
    .setDescription('The Awesome-nest API Documents')
    .setBasePath('api/v1')
    .addBearerAuth()
    .setVersion('0.0.1')
    .build()

const document = SwaggerModule.createDocument(app, options)
SwaggerModule.setup('docs', app, document)
Copy the code

At this point, visit http://localhost:3300/docs can see swagger of the document page.

For different apis you can use @apiusetags () in controller, and for apis that require authentication you can add @apiBearerAuth () so you can test the API directly after you fill in the token in your Swagger:

@ApiUseTags('cats')
@ApiBearerAuth(a)@Controller('cats')
@UseGuards(AuthGuard())
export class CatsController {
  constructor(private readonly catsService: CatsService) {}

  @Get('page')
  @Render('catsPage')
  getCatsPage(): Promise<any> {
    return this.catsService.getCats()
  }
}
Copy the code

For our designated DTO, in order for SwaggerModule to have access to class properties, we must mark all these properties with the @apiModelProperty () decorator:

import { ApiModelProperty } from '@nestjs/swagger'
import { IsEmail, IsString } from 'class-validator'

export class AccountDto {
  @ApiModelProperty(a)@IsString(a)@IsEmail()
  readonly email: string

  @ApiModelProperty(a)@IsString()
  readonly password: string
}
Copy the code

For more usage of swagger documentation, see OpenAPI (Swagger).

Thermal overload

When running NPM run start:dev during development, it is a full compilation. If the project is large, full compilation will take a long time. In this case, we can use WebPack to help us do incremental compilation, which will greatly increase the development efficiency.

First, install webPack-related dependencies:

$ npm i --save-dev webpack webpack-cli webpack-node-externals ts-loader
Copy the code

Create a webpack.config.js in the root directory:

const webpack = require('webpack');
const path = require('path');
const nodeExternals = require('webpack-node-externals');

module.exports = {
  entry: ['webpack/hot/poll? 100 '.'./src/main.ts'],
  watch: true,
  target: 'node',
  externals: [
    nodeExternals({
      whitelist: ['webpack/hot/poll? 100 '],}),],module: {
    rules: [
      {
        test: /.tsx? $/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  mode: 'development',
  resolve: {
    extensions: ['.tsx'.'.ts'.'.js'],
  },
  plugins: [new webpack.HotModuleReplacementPlugin()],
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'server.js',}};Copy the code

Enable HMR in main.ts:

declare const module: any;

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

  if (module.hot) {
    module.hot.accept();
    module.hot.dispose((a)= > app.close());
  }
}
bootstrap();
Copy the code

Add the following two commands to package.json:

{
  "scripts": {
    "start": "node dist/server"."webpack": "webpack --config webpack.config.js"}}Copy the code

After running NPM Run webpack, WebPack starts monitoring files, and then runs NPM Start in another command line window.