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.