我总结了1w字的Nest.js入门最佳实践

发布时间:2023年12月19日

前言

全文1w字,估计10分钟左右阅读完,需要有一定后端基础和Nest.js使用基础。

看完本文你将学会:

  1. Nest.js入门最佳实践
  2. 后端基础架构分层
  3. 权限认证中间件
  4. 集成Swagger
  5. TypeORM操作数据库
  6. 自定义参数校验器

起步

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

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

集成Swagger文档

async function bootstrap() {
  const appOptions = { cors: true };
  const app = await NestFactory.create(ApplicationModule, appOptions);
  app.setGlobalPrefix("api");

  const options = new DocumentBuilder()
    .setTitle("NestJS Realworld Example App")
    .setDescription("The Realworld API description")
    .setVersion("1.0")
    .setBasePath("api")
    .addBearerAuth()
    .build();
  const document = SwaggerModule.createDocument(app, options);
  SwaggerModule.setup("/docs", app, document);

  await app.listen(3000);
}
bootstrap();

在启动函数中,我们首先定义一个 appOptions 对象,设置跨域访问为 true ,使用 app.setGlobalPrefix 方法设置全局前缀为"api" ,这段代码的功能是创建一个基于 NestJS 框架的应用程序,并使用 Swagger 生成 API 文档。我们可以在localhost:3000/docs中打开文档。

我们接着书写 ApplicationModule

初始化 App

// ...
@Module({
  imports: [
    TypeOrmModule.forRoot(),
    ArticleModule,
    UserModule,
    ProfileModule,
    TagModule,
  ],
  controllers: [AppController],
  providers: [],
})
export class ApplicationModule {
  constructor(private readonly connection: Connection) {}
}
  1. imports : imports 属性用于指定当前模块所依赖的其他模块。
  2. controllers : controllers 属性用于指定当前模块中的控制器类。控制器负责处理传入的请求,并返回响应。包括处理权限校验、处理 VO、处理异常等。
  3. providers : providers 属性用于指定当前模块中的提供者。提供者负责创建和管理依赖注入的实例。

ArticleModule

在article这个目录中,我们创建dto、controller、entity实体类、interface、service。接下来我们看下ArticleModule。

// ...、
@Module({
  imports: [
    TypeOrmModule.forFeature([
      ArticleEntity,
      Comment,
      UserEntity,
      FollowsEntity,
    ]),
    UserModule,
  ],
  providers: [ArticleService],
  controllers: [ArticleController],
})
export class ArticleModule implements NestModule {
  public configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(AuthMiddleware)
      .forRoutes(
        { path: "articles/feed", method: RequestMethod.GET },
        { path: "articles", method: RequestMethod.POST },
        { path: "articles/:slug", method: RequestMethod.DELETE },
        { path: "articles/:slug", method: RequestMethod.PUT },
        { path: "articles/:slug/comments", method: RequestMethod.POST },
        { path: "articles/:slug/comments/:id", method: RequestMethod.DELETE },
        { path: "articles/:slug/favorite", method: RequestMethod.POST },
        { path: "articles/:slug/favorite", method: RequestMethod.DELETE }
      );
  }
}

这里ArticleModule实现了NestModule接口,我们在依赖注入的consumer中可以应用中间件AuthMiddleware,而forRoutes表示中间件在路由的应用范围。

实体类编写

import { Entity, PrimaryGeneratedColumn, Column, OneToOne, ManyToOne, OneToMany, JoinColumn, AfterUpdate, BeforeUpdate } from 'typeorm';
import { UserEntity } from '../user/user.entity';
import { Comment } from './comment.entity';

@Entity('article')
export class ArticleEntity {

  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  slug: string;

  @Column()
  title: string;

  @Column({default: ''})
  description: string;

  @Column({default: ''})
  body: string;

  @Column({ type: 'timestamp', default: () => "CURRENT_TIMESTAMP"})
  created: Date;

  @Column({ type: 'timestamp', default: () => "CURRENT_TIMESTAMP"})
  updated: Date;

  @BeforeUpdate()
  updateTimestamp() {
    this.updated = new Date;
  }

  @Column('simple-array')
  tagList: string[];

  @ManyToOne(type => UserEntity, user => user.articles)
  author: UserEntity;

  @OneToMany(type => Comment, comment => comment.article, {eager: true})
  @JoinColumn()
  comments: Comment[];

  @Column({default: 0})
  favoriteCount: number;
}

这里我们使用typeorm库定义实体类:

  1. @Entity('article') : 表示该类对应数据库中的"article"表。
  2. @PrimaryGeneratedColumn() : 表示自动生成的主键列。
  3. @Column() : 表示普通的列。
  4. @BeforeUpdate() : 表示在更新操作之前触发的方法。
  5. @ManyToOne() : 表示多对一的关系,即一个作者可以有多篇文章。
  6. @OneToMany() : 表示一对多的关系,即一篇文章可以有多个评论。
  7. @JoinColumn() : 表示关联表之间的连接列。
  8. @Column('simple-array') : 表示存储为简单数组的列。
  9. @Column({default: ''}) : 表示具有默认值的列。
  10. @Column({ type: 'timestamp', default: () => "CURRENT_TIMESTAMP"}) : 表示具有默认值为当前时间戳的时间戳列。

TypeORM

TypeORM 是一个功能强大且易于使用的 ORM(对象关系映射)框架,它提供了一种简洁的方式来管理数据库模型和查询数据,同时支持多种数据库系统,如 MySQL、PostgreSQL、SQLite、Microsoft SQL Server 等。

TypeORM 的主要特性包括:

  1. 实体映射:使用装饰器将 TypeScript 类与数据库表进行映射,使得操作数据库变得更加直观和易于理解。
  2. 数据库迁移:通过数据库迁移功能,可以轻松地管理数据库模式的变更,包括创建、修改和删除表、列等。
  3. 查询构建器:提供了强大的查询构建器,可以使用链式调用的方式构建复杂的数据库查询,支持筛选、排序、分页等操作。
  4. 事务管理:支持事务操作,可以确保多个数据库操作的原子性,保证数据的一致性。
  5. 关联关系:支持定义实体之间的关联关系,例如一对一、一对多、多对多等关系,并提供方便的方法来进行关联查询。
  6. 数据库连接管理:支持多个数据库连接的管理,可以根据需要连接不同的数据库。
  7. 数据库查询日志:提供详细的数据库查询日志,方便调试和性能优化。
  8. TypeScript:TypeORM 是一个基于 TypeScript 的库,提供了完整的类型定义和类型检查。

接下来我们书写controller层

配置数据库

在typeorm中,我们可以在根目录创建ormconfig.json文件,用于配置数据库。

controller编写

// ...
@ApiBearerAuth()
@ApiTags("articles")
@Controller("articles")
export class ArticleController {
  constructor(private readonly articleService: ArticleService) {}

  @ApiOperation({ summary: "Get all articles" })
  @ApiResponse({ status: 200, description: "Return all articles." })
  @Get()
  async findAll(@Query() query): Promise<ArticlesRO> {
    return await this.articleService.findAll(query);
  }

  @ApiOperation({ summary: "Get article feed" })
  @ApiResponse({ status: 200, description: "Return article feed." })
  @ApiResponse({ status: 403, description: "Forbidden." })
  @Get("feed")
  async getFeed(
    @User("id") userId: number,
    @Query() query
  ): Promise<ArticlesRO> {
    return await this.articleService.findFeed(userId, query);
  }

  @Get(":slug")
  async findOne(@Param("slug") slug): Promise<ArticleRO> {
    return await this.articleService.findOne({ slug });
  }

  @Get(":slug/comments")
  async findComments(@Param("slug") slug): Promise<CommentsRO> {
    return await this.articleService.findComments(slug);
  }

  @ApiOperation({ summary: "Create article" })
  @ApiResponse({
    status: 201,
    description: "The article has been successfully created.",
  })
  @ApiResponse({ status: 403, description: "Forbidden." })
  @Post()
  async create(
    @User("id") userId: number,
    @Body("article") articleData: CreateArticleDto
  ) {
    return this.articleService.create(userId, articleData);
  }

  @ApiOperation({ summary: "Update article" })
  @ApiResponse({
    status: 201,
    description: "The article has been successfully updated.",
  })
  @ApiResponse({ status: 403, description: "Forbidden." })
  @Put(":slug")
  async update(
    @Param() params,
    @Body("article") articleData: CreateArticleDto
  ) {
    // Todo: update slug also when title gets changed
    return this.articleService.update(params.slug, articleData);
  }

  @ApiOperation({ summary: "Delete article" })
  @ApiResponse({
    status: 201,
    description: "The article has been successfully deleted.",
  })
  @ApiResponse({ status: 403, description: "Forbidden." })
  @Delete(":slug")
  async delete(@Param() params) {
    return this.articleService.delete(params.slug);
  }

  @ApiOperation({ summary: "Create comment" })
  @ApiResponse({
    status: 201,
    description: "The comment has been successfully created.",
  })
  @ApiResponse({ status: 403, description: "Forbidden." })
  @Post(":slug/comments")
  async createComment(
    @Param("slug") slug,
    @Body("comment") commentData: CreateCommentDto
  ) {
    return await this.articleService.addComment(slug, commentData);
  }

  @ApiOperation({ summary: "Delete comment" })
  @ApiResponse({
    status: 201,
    description: "The article has been successfully deleted.",
  })
  @ApiResponse({ status: 403, description: "Forbidden." })
  @Delete(":slug/comments/:id")
  async deleteComment(@Param() params) {
    const { slug, id } = params;
    return await this.articleService.deleteComment(slug, id);
  }

  @ApiOperation({ summary: "Favorite article" })
  @ApiResponse({
    status: 201,
    description: "The article has been successfully favorited.",
  })
  @ApiResponse({ status: 403, description: "Forbidden." })
  @Post(":slug/favorite")
  async favorite(@User("id") userId: number, @Param("slug") slug) {
    return await this.articleService.favorite(userId, slug);
  }

  @ApiOperation({ summary: "Unfavorite article" })
  @ApiResponse({
    status: 201,
    description: "The article has been successfully unfavorited.",
  })
  @ApiResponse({ status: 403, description: "Forbidden." })
  @Delete(":slug/favorite")
  async unFavorite(@User("id") userId: number, @Param("slug") slug) {
    return await this.articleService.unFavorite(userId, slug);
  }
}
  1. @ApiBearerAuth(): 表示需要进行身份验证的 API,需要在请求头中包含有效的身份验证令牌。
  2. @User("id"): 表示获取经过身份验证的用户的 ID。
  @ApiOperation({ summary: "Get article feed" })
  @ApiResponse({ status: 200, description: "Return article feed." })
  @ApiResponse({ status: 403, description: "Forbidden." })
  @Get("feed")
  async getFeed(
    @User("id") userId: number,
    @Query() query
  ): Promise<ArticlesRO> {
    return await this.articleService.findFeed(userId, query);
  }

我们注意下getFeed接口,用于查询用户的关注动态文章的方法。根据传入的用户ID和查询参数,查询用户关注的用户的文章,并返回文章列表和文章总数。而这个@User中传入的id就是data,我们会去校验并最终返回值赋值给userId。

接下来我们看下注解@User是如何实现的:

权限认证

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { SECRET } from '../config';
import * as jwt from 'jsonwebtoken';

export const User = createParamDecorator((data: any, ctx: ExecutionContext) => {
  const req = ctx.switchToHttp().getRequest();
  
  if (!!req.user) {
    return !!data ? req.user[data] : req.user;
  }

  // in case a route is not protected, we still want to get the optional auth user from jwt
  const token = req.headers.authorization ? (req.headers.authorization as string).split(' ') : null;
  if (token && token[1]) {
    const decoded: any = jwt.verify(token[1], SECRET);
    return !!data ? decoded[data] : decoded.user;
  }
});

createParamDecorator 自定义参数装饰器

我们通过 Nest.js 自定义参数装饰器createParamDecorator,从请求中获取经过身份验证的用户信息。如果我们传入id,可以在auth.middleware权限中间件中在挂载req.user中获取信息。然后根据请求头加密信息将通过jwt的解密方法获取最终解码的用户信息。

我们接下来看看权限中间件是如何实现的:

AuthMiddleware 权限中间件

import { HttpException } from '@nestjs/common/exceptions/http.exception';
import { NestMiddleware, HttpStatus, Injectable } from '@nestjs/common';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { Request, Response, NextFunction } from 'express';
import * as jwt from 'jsonwebtoken';
import { SECRET } from '../config';
import { UserService } from './user.service';

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  constructor(private readonly userService: UserService) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const authHeaders = req.headers.authorization;
    if (authHeaders && (authHeaders as string).split(' ')[1]) {
      const token = (authHeaders as string).split(' ')[1];
      const decoded: any = jwt.verify(token, SECRET);
      const user = await this.userService.findById(decoded.id);

      if (!user) {
        throw new HttpException('User not found.', HttpStatus.UNAUTHORIZED);
      }

      req.user = user.user;
      next();

    } else {
      throw new HttpException('Not authorized.', HttpStatus.UNAUTHORIZED);
    }
  }
}

AuthMiddleware中间件用于验证请求中的身份验证令牌,并将解码后的用户信息附加到请求对象中。如果令牌有效且对应的用户存在,则请求会继续传递给下一个中间件或路由处理方法。否则,将抛出适当的 HTTP 异常来表示未授权的访问。

service业务层

import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository, getRepository, DeleteResult } from "typeorm";
import { ArticleEntity } from "./article.entity";
import { Comment } from "./comment.entity";
import { UserEntity } from "../user/user.entity";
import { FollowsEntity } from "../profile/follows.entity";
import { CreateArticleDto } from "./dto";

import { ArticleRO, ArticlesRO, CommentsRO } from "./article.interface";
const slug = require("slug");

@Injectable()
export class ArticleService {
  constructor(
    @InjectRepository(ArticleEntity)
    private readonly articleRepository: Repository<ArticleEntity>,
    @InjectRepository(Comment)
    private readonly commentRepository: Repository<Comment>,
    @InjectRepository(UserEntity)
    private readonly userRepository: Repository<UserEntity>,
    @InjectRepository(FollowsEntity)
    private readonly followsRepository: Repository<FollowsEntity>
  ) {}

   // ...
  async findFeed(userId: number, query): Promise<ArticlesRO> {
    const _follows = await this.followsRepository.find({ followerId: userId });
    
    // 校验
    if (!(Array.isArray(_follows) && _follows.length > 0)) {
      return { articles: [], articlesCount: 0 };
    }

    const ids = _follows.map((el) => el.followingId);
    
    // 查找所有关注的多个文章记录
    const qb = await getRepository(ArticleEntity)
      .createQueryBuilder("article")
      .where("article.authorId IN (:ids)", { ids });
    
    // 设置排序
    qb.orderBy("article.created", "DESC");
    
    // 返回total
    const articlesCount = await qb.getCount();
    
    // 做分页处理
    if ("limit" in query) {
      qb.limit(query.limit);
    }

    if ("offset" in query) {
      qb.offset(query.offset);
    }

    const articles = await qb.getMany();

    return { articles, articlesCount };
  }

@InjectRepository() 是一个由 TypeORM 提供的装饰器,用于在 Nest.js 中将仓库(Repository)注入到类的属性中。在类中就可以使用 articleRepository 属性来访问和操作与 ArticleEntity 相关的数据库表。

另外,需要注意的也可以直接使用 getRepository 可以直接获取到实体类对应的仓库。

分层结构

接下来我用一张图总结下基础的分层结构

总结

本文介绍了如何在 Nest.js 中集成 Swagger,并使用 TypeORM 进行数据库操作。通过使用 Swagger,我们可以自动生成 API 文档,方便开发人员查看和测试 API。使用 TypeORM,我们可以轻松地进行数据库操作,包括创建、更新、删除和查询数据。同时,我们还介绍了如何使用自定义参数装饰器和中间件进行身份验证,以及如何使用自定义注解和装饰器来定义路由和请求方法。通过这些技术,我们可以更好地组织和管理代码,并提高开发效率。

觉得不错可以点赞、关注、收藏,感谢你的支持!

转发本文+关注+私信【学习】或添加下方下助理即可领取更多资料

原文链接:
https://juejin.cn/post/7270464435297189900

文章来源:https://blog.csdn.net/Perback/article/details/135088864
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。