全文1w字,估计10分钟左右阅读完,需要有一定后端基础和Nest.js使用基础。
看完本文你将学会:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
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
// ...
@Module({
imports: [
TypeOrmModule.forRoot(),
ArticleModule,
UserModule,
ProfileModule,
TagModule,
],
controllers: [AppController],
providers: [],
})
export class ApplicationModule {
constructor(private readonly connection: Connection) {}
}
在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库定义实体类:
TypeORM 是一个功能强大且易于使用的 ORM(对象关系映射)框架,它提供了一种简洁的方式来管理数据库模型和查询数据,同时支持多种数据库系统,如 MySQL、PostgreSQL、SQLite、Microsoft SQL Server 等。
TypeORM 的主要特性包括:
接下来我们书写controller层
在typeorm中,我们可以在根目录创建ormconfig.json文件,用于配置数据库。
// ...
@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);
}
}
@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;
}
});
我们通过 Nest.js 自定义参数装饰器createParamDecorator,从请求中获取经过身份验证的用户信息。如果我们传入id,可以在auth.middleware权限中间件中在挂载req.user中获取信息。然后根据请求头加密信息将通过jwt的解密方法获取最终解码的用户信息。
我们接下来看看权限中间件是如何实现的:
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 异常来表示未授权的访问。
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