1w字Nest.js入门最佳实践

全文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
  ) {
    
    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;
  }

  
  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");
    
    
    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,我们可以轻松地进行数据库操作,包括创建、更新、删除和查询数据。同时,我们还介绍了如何使用自定义参数装饰器和中间件进行身份验证,以及如何使用自定义注解和装饰器来定义路由和请求方法。通过这些技术,我们可以更好地组织和管理代码,并提高开发效率。

作者:谦宇

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
一套目前来说最好的nestjs实战教程,提供QQ长期问答服务. 本人从 08 年到 18 年一直从事于 PHP 的开发。从 18 年开始转向 Typescript+React+Nestjs 的技术栈。目前来说 React 应该是一个非常好用的前端框架,生态非常完善,并且十分灵活简单。Nestjs 则是 Node.js 中唯一且无敌存在的后端 web 框架。因为我个人从事这套技术栈开发已经 4 年多,所以颇有心得,做了这套 React18 视频教程和 Nestjs 实战视频教程。现在视频教程也是刚刚开始做了一部分,还在持续更新。使用 TS 全栈开发可以基本涵盖各种平台的方方面面,比如开发桌面应用的 Electron, 开发小程序的 Taro, 开发 Spa 中后台的 React,开发 SSR 网站的 next.js开发移动应用的 React Native, 开发 CLI 的 Yargs, 以及开发后端的 Nestjs。基本学会一套,全面够用,再加上 Monorepo 组织结构,一个仓库所有平台都可以搞定。 包含以下知识点 - 掌握Nestjs框架的依赖注入,模块,提供者,生命周期等概念- 掌握DTO数据验证,响应序列化,异常过滤器等常用功能- 学会编写一些常用的class-validator验证约束- 熟练掌握Typeorm以及Nestjs与Typeorm结合开发- 学会整合Swagger输出Open API文档- 掌握TS装饰器以及反射元数据的定义和使用- 编写一些数据库相关的数据验证约束(比如树形表的同级别某段唯一验证等)- 学会通过继承并魔改Nestjs源码编写自定义的全局验证器- 可以编写自定义的配置系统以及核心功能包- 学会自定义的代码组织方式(比如教程中我把默认的Nestjs应用改成Util+PluginModule模式)- 掌握编写一些常用的Util仓库(比如数据库,Redis,Restful)- 利用Yargs结合魔改后的框架可以编写一些自定义CLI命令(比如数据迁移,数据填充等)- 掌握如何利用阿里云/腾讯云推送邮件和短信- 掌握使用消息列队(MQ)的方式异步推送邮件和短信- 掌握守卫原理以及编写一些用户验证的守卫- 编写一个完善的用户系统(JWT认证,短信/邮件登录,短信/邮件注册,找回密码,绑定手机和邮箱等)- 熟练地通过编写装饰器去实现一些常用的功能- 通过SSE,WebSockets实现用户的上线,下线以及消息实时推送,消息广播等- 学会使用云存储来上传文件- 学会大文件断点雪川- 实现RBAC的权限系统- 理解请求范围概念以及性能方便的考量- 自己构建配置系统,实现配置验证以及通过YAML或数据库来进行动态配置- 通过适用Vscode进行Debug以及编写Jest测试来提升开发效率与程序的可用性- 学会使用Node来编写自定义的CLI命令- 利用NestCURD进行快速开发- 学会Graphql替代Restful写API- 使用Mongodb替代关系型数据库- 掌握一些常用的打包工具,比如通过ncc打包成单文件,通过pack打包成二进制等- 学会一些常用的部署方式,比如通过nginx+pm2反向代理部署,devops自动化CI,CD等- 学会使用pnpm workspaces来使用monreopo组织代码
Nest.js中,你可以使用crypto-js库进行加密和解密操作。crypto-js是一个JavaScript加密库,它提供了各种常见的加密算法,如AES、DES、SHA等。要在Nest.js中使用crypto-js,你需要先安装它。 首先,在你的Nest.js项目中打开终端,并运行以下命令来安装crypto-js库: ```bash npm install crypto-js ``` 安装完成后,你可以在你的代码中引入crypto-js并使用它的加密和解密功能。以下是一个简单的示例: ```typescript import { Injectable } from "@nestjs/common"; import * as CryptoJS from 'crypto-js'; @Injectable() export class CryptoUtil { encrypt(text: string, secretKey: string): string { return CryptoJS.AES.encrypt(text, secretKey).toString(); } decrypt(ciphertext: string, secretKey: string): string { const bytes = CryptoJS.AES.decrypt(ciphertext, secretKey); return bytes.toString(CryptoJS.enc.Utf8); } } ``` 上面的代码示例展示了如何在Nest.js的CryptoUtil类中使用crypto-js库进行加密和解密操作。在encrypt方法中,我们使用AES算法和给定的密钥对文本进行加密。在decrypt方法中,我们使用相同的密钥对密文进行解密。 你可以根据自己的需求调整和扩展这个示例。请注意,为了使用crypto-js库,你需要在文件头部引入CryptoJS,并使用`import * as CryptoJS from 'crypto-js';`语句。 希望这个示例对你理解在Nest.js中使用crypto-js进行加解密有所帮助。如果你还有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Web面试那些事儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值