所有文章 > API产品 > 掌握 NestJS — 构建高效的 REST API 后端

掌握 NestJS — 构建高效的 REST API 后端

当我们着手开发REST API后端服务时,NodeJS无疑是一个重量级选择。NodeJS作为一个Javascript运行时,使得Javascript代码能够在非浏览器环境中执行。为了构建网络服务,我们通常依赖于像Express和NestJS这样的框架。

NestJS 有一个特定的设计理念,这有助于开发一个良好的项目结构。核心框架部分如下:

  1. Controller (控制器) — 它负责定义API的终端节点及其处理方式。
  2. Service — 这是一种可选模式,旨在将控制器与业务逻辑相分离。服务类会利用其他组件(如Model、Cache以及其他服务)来协助控制器处理请求。
  3. Module — 它有助于通过导入和导出实例来解决控制器、服务和其他组件的依赖关系。其他模块访问导出的组件以满足其依赖关系。
  4. APP_GUARD — 它适用于全局所有控制器路由。其执行顺序取决于提供程序列表中的位置。当请求路由到控制器时,它首先通过 Guard(我们确实可以选择将 Guard 应用于特定控制器或控制器的方法)。 Guard通常用于 Authentication 和 Authorization。
  5. APP_PIPE — 它应用全局管道来转换和验证所有传入请求,然后再到达控制器路由处理程序。
  6. APP_INTERCEPTOR – 它用于在将数据发送给客户端之前对其进行转换。这可以实现统一的API响应格式或响应验证。
  7. APP_FILTER — 它用于定义中央异常处理和以通用格式发送错误响应。
  8. Annotations — NestJS 框架的主要功能之一是大量使用注释。它们用于通过模块接收实例、定义验证规则等。

您将会了解如何在wimm-node-app项目中实现功能。但在此之前,我们先来探讨一下如何定义功能。采用特性封装是一个明智的选择,即将与特定功能相关的内容大多保存在一个单独的特性目录中。这里的功能指的是常见的路由基础URL,例如/blog和/content分别代表两个不同的功能。一般而言,功能具有以下结构:

  1. dto — 它表示请求和响应正文。我们对 DTO 应用所需的验证。
  2. schema — 它包含 mongo 集合的模型(使用 mongo,否则使用任何其他 ORM 模型)。
  3. controller — 它定义路由处理程序函数等。
  4. service — 它通过业务逻辑协助控制器

注意:为了继续阅读,您应该克隆GitHub存储库wimm-node-app。

让我们看看项目中的 mentor 示例

mentor
├── dto
│ ├── create-mentor.dto.ts
│ ├── mentor-info.dto.ts
│ └── update-mentor.dto.ts
├── schemas
│ └── mentor.schema.ts
├── mentor-admin.controller.ts
├── mentor.controller.ts
├── mentor.module.ts
├── mentor.service.ts
└── mentors.controller.ts

create-mentor.dto.ts

import {
IsOptional,
IsUrl,
Max,
MaxLength,
Min,
MinLength,
} from 'class-validator';

export class CreateMentorDto {
@MinLength(3)
@MaxLength(50)
readonly name: string;

@MinLength(3)
@MaxLength(50)
readonly occupation: string;

@MinLength(3)
@MaxLength(300)
readonly title: string;

@MinLength(3)
@MaxLength(10000)
readonly description: string;

@IsUrl({ require_tld: false })
@MaxLength(300)
readonly thumbnail: string;

@IsUrl({ require_tld: false })
@MaxLength(300)
readonly coverImgUrl: string;

@IsOptional()
@Min(0)
@Max(1)
readonly score: number;

constructor(params: CreateMentorDto) {
Object.assign(this, params);
}
}

mentor.schema.ts

import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
import mongoose, { HydratedDocument, Types } from 'mongoose';
import { User } from '../../user/schemas/user.schema';

export type MentorDocument = HydratedDocument<Mentor>;

@Schema({ collection: 'mentors', versionKey: false, timestamps: true })
export class Mentor {
readonly _id: Types.ObjectId;

@Prop({ required: true, maxlength: 50, trim: true })
name: string;

@Prop({ required: true, maxlength: 300, trim: true })
title: string;

@Prop({ required: true, maxlength: 300, trim: true })
thumbnail: string;

@Prop({ required: true, maxlength: 50, trim: true })
occupation: string;

@Prop({ required: true, maxlength: 10000, trim: true })
description: string;

@Prop({ required: true, maxlength: 300, trim: true })
coverImgUrl: string;

@Prop({ default: 0.01, max: 1, min: 0 })
score: number;

@Prop({
type: mongoose.Schema.Types.ObjectId,
ref: User.name,
required: true,
})
createdBy: User;

@Prop({
type: mongoose.Schema.Types.ObjectId,
ref: User.name,
required: true,
})
updatedBy: User;

@Prop({ default: true })
status: boolean;
}

export const MentorSchema = SchemaFactory.createForClass(Mentor);

MentorSchema.index(
{ name: 'text', occupation: 'text', title: 'text' },
{ weights: { name: 5, occupation: 1, title: 2 }, background: false },
);

MentorSchema.index({ _id: 1, status: 1 });

mentor.controller.ts

import { Controller, Get, NotFoundException, Param } from '@nestjs/common';
import { Types } from 'mongoose';
import { MongoIdTransformer } from '../common/mongoid.transformer';
import { MentorService } from './mentor.service';
import { MentorInfoDto } from './dto/mentor-info.dto';

@Controller('mentor')
export class MentorController {
constructor(private readonly mentorService: MentorService) {}

@Get('id/:id')
async findOne(
@Param('id', MongoIdTransformer) id: Types.ObjectId,
): Promise<MentorInfoDto> {
const mentor = await this.mentorService.findById(id);
if (!mentor) throw new NotFoundException('Mentor Not Found');
return new MentorInfoDto(mentor);
}
}

mentor.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectModel } from '@nestjs/mongoose';
import { Model, Types } from 'mongoose';
import { Mentor } from './schemas/mentor.schema';
import { User } from '../user/schemas/user.schema';
import { CreateMentorDto } from './dto/create-mentor.dto';
import { UpdateMentorDto } from './dto/update-mentor.dto';
import { PaginationDto } from '../common/pagination.dto';

@Injectable()
export class MentorService {
constructor(
@InjectModel(Mentor.name) private readonly mentorModel: Model<Mentor>,
) {}

INFO_PARAMETERS = '-description -status';

async create(admin: User, createMentorDto: CreateMentorDto): Promise<Mentor> {
const created = await this.mentorModel.create({
...createMentorDto,
createdBy: admin,
updatedBy: admin,
});
return created.toObject();
}

async findById(id: Types.ObjectId): Promise<Mentor | null> {
return this.mentorModel.findOne({ _id: id, status: true }).lean().exec();
}

async search(query: string, limit: number): Promise<Mentor[]> {
return this.mentorModel
.find({
$text: { $search: query, $caseSensitive: false },
status: true,
})
.select(this.INFO_PARAMETERS)
.limit(limit)
.lean()
.exec();
}

...
}

现在,让我们看看项目结构的概述。

  1. src — 应用程序源代码
  2. test  — E2E 集成测试
  3. disk — 子模块:服务器文件存储(仅用于演示目的)
  4. keys — JWT 令牌的 RSA 密钥
  5. 其余部分是用于构建项目的配置文件

让我们更深入地研究 src 目录

  1. config —我们在 .env 文件中定义环境变量并将它们作为配置加载。

database.config.ts

import { registerAs } from '@nestjs/config';

export const DatabaseConfigName = 'database';

export interface DatabaseConfig {
name: string;
host: string;
port: number;
user: string;
password: string;
minPoolSize: number;
maxPoolSize: number;
}

export default registerAs(DatabaseConfigName, () => ({
name: process.env.DB_NAME || '',
host: process.env.DB_HOST || '',
port: process.env.DB_PORT || '',
user: process.env.DB_USER || '',
password: process.env.DB_USER_PWD || '',
minPoolSize: parseInt(process.env.DB_MIN_POOL_SIZE || '5'),
maxPoolSize: parseInt(process.env.DB_MAX_POOL_SIZE || '10'),
}));

2. setup — 它定义数据库连接和自定义 Winston 记录器

/setup/database.factory.ts

import { Injectable, Logger } from '@nestjs/common';
import {
MongooseOptionsFactory,
MongooseModuleOptions,
} from '@nestjs/mongoose';
import { ConfigService } from '@nestjs/config';
import { DatabaseConfig, DatabaseConfigName } from '../config/database.config';
import mongoose from 'mongoose';
import { ServerConfig, ServerConfigName } from '../config/server.config';

@Injectable()
export class DatabaseFactory implements MongooseOptionsFactory {
constructor(private readonly configService: ConfigService) {}

createMongooseOptions(): MongooseModuleOptions {
const dbConfig =
this.configService.getOrThrow<DatabaseConfig>(DatabaseConfigName);

const { user, host, port, name, minPoolSize, maxPoolSize } = dbConfig;

const password = encodeURIComponent(dbConfig.password);

const uri = `mongodb://${user}:${password}@${host}:${port}/${name}`;

const serverConfig =
this.configService.getOrThrow<ServerConfig>(ServerConfigName);
if (serverConfig.nodeEnv == 'development') mongoose.set({ debug: true });

Logger.debug('Database URI:' + uri);

return {
uri: uri,
autoIndex: true,
minPoolSize: minPoolSize,
maxPoolSize: maxPoolSize,
connectTimeoutMS: 60000, // Give up initial connection after 10 seconds
socketTimeoutMS: 45000, // Close sockets after 45 seconds of inactivity,
};
}
}

3. app.module.ts — 它加载我们应用程序的所有其他模块和配置。

@Module({
imports: [
ConfigModule.forRoot({
load: [
serverConfig,
databaseConfig,
cacheConfig,
authkeyConfig,
tokenConfig,
diskConfig,
],
cache: true,
envFilePath: getEnvFilePath(),
}),
MongooseModule.forRootAsync({
imports: [ConfigModule],
useClass: DatabaseFactory,
}),
RedisCacheModule,
CoreModule,
AuthModule,
MessageModule,
FilesModule,
ScrapperModule,
MentorModule,
TopicModule,
SubscriptionModule,
ContentModule,
BookmarkModule,
SearchModule,
],
providers: [
{
provide: 'Logger',
useClass: WinstonLogger,
},
],
})
export class AppModule {}

function getEnvFilePath() {
return process.env.NODE_ENV === 'test' ? '.env.test' : '.env';
}

4. main.ts — 这是服务器运行时执行的第一个脚本。它通过加载 AppModule 创建一个 Nest 应用程序。

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ConfigService } from '@nestjs/config';
import { ServerConfig, ServerConfigName } from './config/server.config';

async function server() {
const app = await NestFactory.create(AppModule);

const configService = app.get(ConfigService);
const serverConfig = configService.getOrThrow<ServerConfig>(ServerConfigName);

await app.listen(serverConfig.port);
}

server();

现在,我们可以继续探索有关架构的更多信息。首先要的是了解核心模块。core 包含我们架构的构建块。

为了使我们的服务保持一致,我们需要为请求和响应定义一个结构。REST API 将发送 2 种类型的响应:

// 1. Message Response
{
"statusCode": 10000,
"message": "something",
}

// 2. Data Response
{
"statusCode": 10000,
"message": "something",
"data": {DTO}
}

我们将创建类来表示这个结构 — src/core/http/response.ts

export enum StatusCode {
SUCCESS = 10000,
FAILURE = 10001,
RETRY = 10002,
INVALID_ACCESS_TOKEN = 10003,
}

export class MessageResponse {
readonly statusCode: StatusCode;
readonly message: string;

constructor(statusCode: StatusCode, message: string) {
this.statusCode = statusCode;
this.message = message;
}
}

export class DataResponse<T> extends MessageResponse {
readonly data: T;

constructor(statusCode: StatusCode, message: string, data: T) {
super(statusCode, message);
this.data = data;
}
}

现在,我们还有 3 种类型的请求 — Public、Private 和 Protected。我们在 src/core/http/request.ts 中定义它们

import { Request } from 'express';
import { User } from '../../user/schemas/user.schema';
import { ApiKey } from '../../auth/schemas/apikey.schema';
import { Keystore } from '../../auth/schemas/keystore.schema';

export interface PublicRequest extends Request {
apiKey: ApiKey;
}

export interface RoleRequest extends PublicRequest {
currentRoleCodes: string[];
}

export interface ProtectedRequest extends RoleRequest {
user: User;
accessToken: string;
keystore: Keystore;
}

此外,当控制器返回 DTO 时,我们需要做 2 件事:

  1. 响应验证 — src/core/interceptors/response.validations.ts
// response-validation.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
InternalServerErrorException,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ValidationError, validateSync } from 'class-validator';

@Injectable()
export class ResponseValidation implements NestInterceptor {
intercept(_: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
if (data instanceof Object) {
const errors = validateSync(data);
if (errors.length > 0) {
const messages = this.extractErrorMessages(errors);
throw new InternalServerErrorException([
'Response validation failed',
...messages,
]);
}
}
return data;
}),
);
}

private extractErrorMessages(
errors: ValidationError[],
messages: string[] = [],
): string[] {
for (const error of errors) {
if (error) {
if (error.children && error.children.length > 0)
this.extractErrorMessages(error.children, messages);
const constraints = error.constraints;
if (constraints) messages.push(Object.values(constraints).join(', '));
}
}
return messages;
}
}

2. 响应转换 — 将 DTO 转换为响应对象。

src/core/interceptors/response.transformer.ts

import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, map } from 'rxjs';
import { DataResponse, MessageResponse, StatusCode } from '../http/response';

@Injectable()
export class ResponseTransformer implements NestInterceptor {
intercept(_: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
map((data) => {
if (data instanceof MessageResponse) return data;
if (data instanceof DataResponse) return data;
if (typeof data == 'string')
return new MessageResponse(StatusCode.SUCCESS, data);
return new DataResponse(StatusCode.SUCCESS, 'success', data);
}),
);
}
}

最后,我们还必须在 src/core/interceptors/exception.handler.ts 中定义异常处理过滤器

// exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
HttpStatus,
InternalServerErrorException,
UnauthorizedException,
} from '@nestjs/common';
import { TokenExpiredError } from '@nestjs/jwt';
import { Request, Response } from 'express';
import { StatusCode } from '../http/response';
import { isArray } from 'class-validator';
import { ConfigService } from '@nestjs/config';
import { ServerConfig, ServerConfigName } from '../../config/server.config';
import { WinstonLogger } from '../../setup/winston.logger';

@Catch()
export class ExpectionHandler implements ExceptionFilter {
constructor(
private readonly configService: ConfigService,
private readonly logger: WinstonLogger,
) {}

catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();

let status = HttpStatus.INTERNAL_SERVER_ERROR;
let statusCode = StatusCode.FAILURE;
let message: string = 'Something went wrong';
let errors: any[] | undefined = undefined;

if (exception instanceof HttpException) {
status = exception.getStatus();
const body = exception.getResponse();
if (typeof body === 'string') {
message = body;
} else if ('message' in body) {
if (typeof body.message === 'string') {
message = body.message;
} else if (isArray(body.message) && body.message.length > 0) {
message = body.message[0];
errors = body.message;
}
}
if (exception instanceof InternalServerErrorException) {
this.logger.error(exception.message, exception.stack);
}

if (exception instanceof UnauthorizedException) {
if (message.toLowerCase().includes('invalid access token')) {
statusCode = StatusCode.INVALID_ACCESS_TOKEN;
response.appendHeader('instruction', 'logout');
}
}
} else if (exception instanceof TokenExpiredError) {
status = HttpStatus.UNAUTHORIZED;
statusCode = StatusCode.INVALID_ACCESS_TOKEN;
response.appendHeader('instruction', 'refresh_token');
message = 'Token Expired';
} else {
const serverConfig =
this.configService.getOrThrow<ServerConfig>(ServerConfigName);
if (serverConfig.nodeEnv === 'development') message = exception.message;
this.logger.error(exception.message, exception.stack);
}

response.status(status).json({
statusCode: statusCode,
message: message,
errors: errors,
url: request.url,
});
}
}

我们将创建一个 CoreModule 来应用它们。然后将 CoreModule 添加到 AppModule 中。

import { Module, ValidationPipe } from '@nestjs/common';
import { APP_FILTER, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
import { ResponseTransformer } from './interceptors/response.transformer';
import { ExpectionHandler } from './interceptors/exception.handler';
import { ResponseValidation } from './interceptors/response.validations';
import { ConfigModule } from '@nestjs/config';
import { WinstonLogger } from '../setup/winston.logger';
import { CoreController } from './core.controller';

@Module({
imports: [ConfigModule],
providers: [
{ provide: APP_INTERCEPTOR, useClass: ResponseTransformer },
{ provide: APP_INTERCEPTOR, useClass: ResponseValidation },
{ provide: APP_FILTER, useClass: ExpectionHandler },
{
provide: APP_PIPE,
useValue: new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
}),
},
WinstonLogger,
],
controllers: [CoreController],
})
export class CoreModule {}

下一个重要功能是 auth,它提供 ApiKeyGuardAuthGuard(身份验证)和 RolesGuard(授权)。

src/auth/guards/apikey.guard.ts — 验证了 x-api-key 标头及其权限。

import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
} from '@nestjs/common';
import { HeaderName } from '../../core/http/header';
import { Reflector } from '@nestjs/core';
import { Permissions } from '../decorators/permissions.decorator';
import { PublicRequest } from '../../core/http/request';
import { Permission } from '../../auth/schemas/apikey.schema';
import { AuthService } from '../auth.service';

@Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(
private readonly authService: AuthService,
private readonly reflector: Reflector,
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const permissions = this.reflector.get(Permissions, context.getClass()) ?? [
Permission.GENERAL,
];
if (!permissions) throw new ForbiddenException();

const request = context.switchToHttp().getRequest<PublicRequest>();

const key = request.headers[HeaderName.API_KEY]?.toString();
if (!key) throw new ForbiddenException();

const apiKey = await this.authService.findApiKey(key);
if (!apiKey) throw new ForbiddenException();

request.apiKey = apiKey;

for (const askedPermission of permissions) {
for (const allowedPemission of apiKey.permissions) {
if (allowedPemission === askedPermission) return true;
}
}

throw new ForbiddenException();
}
}

src/auth/guards/auth.guard.ts — 验证 JWT Authentication 标头。它还将 user 和 keystore 添加到请求对象中,供其他处理程序接收。

import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
import { ProtectedRequest } from '../../core/http/request';
import { Types } from 'mongoose';
import { AuthService } from '../auth.service';
import { UserService } from '../../user/user.service';

@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly authService: AuthService,
private readonly reflector: Reflector,
private readonly userService: UserService,
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;

const request = context.switchToHttp().getRequest<ProtectedRequest>();
const token = this.extractTokenFromHeader(request);
if (!token) throw new UnauthorizedException();

const payload = await this.authService.verifyToken(token);
const valid = this.authService.validatePayload(payload);
if (!valid) throw new UnauthorizedException('Invalid Access Token');

const user = await this.userService.findUserById(
new Types.ObjectId(payload.sub),
);
if (!user) throw new UnauthorizedException('User not registered');

const keystore = await this.authService.findKeystore(user, payload.prm);
if (!keystore) throw new UnauthorizedException('Invalid Access Token');

request.user = user;
request.keystore = keystore;

return true;
}

private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}

src/auth/guards/roles.guard.ts — 它验证了给定控制器或控制器处理程序的用户角色。

要在 Controller 中指定 Roles,我们在 src/auth/decorators/role.decorator.ts 中定义装饰器

import { Reflector } from '@nestjs/core';
import { RoleCode } from '../schemas/role.schema';

export const Roles = Reflector.createDecorator<RoleCode[]>();

我们将这个装饰器应用到 Controller 上。示例:src/mentor/mentor-admin.controller.ts

@Roles([RoleCode.ADMIN])
@Controller('mentor/admin')
export class MentorAdminController {
...
}

最后 — src/auth/guards/roles.guard.ts

import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from '../decorators/roles.decorator';
import { ProtectedRequest } from '../../core/http/request';

@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
let roles = this.reflector.get(Roles, context.getHandler());
if (!roles) roles = this.reflector.get(Roles, context.getClass());
if (roles) {
const request = context.switchToHttp().getRequest<ProtectedRequest>();
const user = request.user;
if (!user) throw new ForbiddenException('Permission Denied');

const hasRole = () =>
user.roles.some((role) => !!roles.find((item) => item === role.code));

if (!hasRole()) throw new ForbiddenException('Permission Denied');
}

return true;
}
}

我们现在可以通过图表看到完整的情况 — 整个架构的请求旅程,从而产生响应。

架构中还添加了一些高效的工具。示例 — 验证 id param 字符串并将其转换为 MongoId 对象。让我们看看如何使用 MongoIdTransformer 处理 mongo id 参数。

import { Controller, Get, NotFoundException, Param } from '@nestjs/common';
import { Types } from 'mongoose';
import { MongoIdTransformer } from '../common/mongoid.transformer';
import { MentorService } from './mentor.service';
import { MentorInfoDto } from './dto/mentor-info.dto';

@Controller('mentor')
export class MentorController {
constructor(private readonly mentorService: MentorService) {}

@Get('id/:id')
async findOne(
@Param('id', MongoIdTransformer) id: Types.ObjectId,
): Promise<MentorInfoDto> {
const mentor = await this.mentorService.findById(id);
if (!mentor) throw new NotFoundException('Mentor Not Found');
return new MentorInfoDto(mentor);
}
}

MongoIdTransformer 在 src/common/mongoid.transformer.ts 中实现

import {
PipeTransform,
Injectable,
BadRequestException,
ArgumentMetadata,
} from '@nestjs/common';
import { Types } from 'mongoose';

@Injectable()
export class MongoIdTransformer implements PipeTransform<any> {
transform(value: any, metadata: ArgumentMetadata): any {
if (typeof value !== 'string') return value;

if (metadata.metatype?.name === 'ObjectId') {
if (!Types.ObjectId.isValid(value)) {
const key = metadata?.data ?? '';
throw new BadRequestException(`${key} must be a mongodb id`);
}
return new Types.ObjectId(value);
}

return value;
}
}

同样,我们定义 IsMongoIdObject 验证以在 DTO 中使用。

export class ContentInfoDto {
@IsMongoIdObject()
_id: Types.ObjectId;

...
}

IsMongoIdObject 在以下位置实现:

src/common/mongo.validation.ts

import {
registerDecorator,
ValidationArguments,
ValidationOptions,
} from 'class-validator';
import { Types } from 'mongoose';

export function IsMongoIdObject(validationOptions?: ValidationOptions) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'IsMongoIdObject',
target: object.constructor,
propertyName: propertyName,
constraints: [],
options: validationOptions,
validator: {
validate(value: any) {
return Types.ObjectId.isValid(value);
},

defaultMessage(validationArguments?: ValidationArguments) {
const property = validationArguments?.property ?? '';
return `${property} should be a valid MongoId`;
},
},
});
};
}

架构中隐藏着许多服务于关键功能的微妙细节,这些细节在您阅读代码时可以逐一探索。

Web服务器中,缓存是一个至关重要的工具。在这个项目中,我们采用了Redis作为内存缓存解决方案。

Redis的包装器位于src/cache/redis-cache.ts文件中,它实现了Nest的缓存管理器功能。这一实现通过Nest的缓存API,为我们提供了一个自定义的CacheInterceptor。这里需要注意的是,我不会深入讨论这段代码的具体实现,因为它是内部机制,不建议进行修改。

接下来,我们创建了一个工厂、一个服务以及一个模块,这些组件共同协作,为应用程序启用了Redis缓存功能。

src/cache/cache.factory.ts

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { CacheConfig, CacheConfigName } from '../config/cache.config';
import { redisStore } from './redis-cache';
import { CacheModuleOptions, CacheOptionsFactory } from '@nestjs/cache-manager';

@Injectable()
export class CacheConfigFactory implements CacheOptionsFactory {
constructor(private readonly configService: ConfigService) {}

async createCacheOptions(): Promise<CacheModuleOptions> {
const cacheConfig =
this.configService.getOrThrow<CacheConfig>(CacheConfigName);
const redisURL = `redis://:${cacheConfig.password}@${cacheConfig.host}:${cacheConfig.port}`;
return {
store: redisStore,
url: redisURL,
ttl: cacheConfig.ttl,
};
}
}

src/cache/cache.service.ts

import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Inject, Injectable } from '@nestjs/common';
import { Cache } from 'cache-manager';
import { RedisStore } from './redis-cache';

@Injectable()
export class CacheService {
constructor(@Inject(CACHE_MANAGER) private readonly cache: Cache) {}

async getValue(key: string): Promise<string | null | undefined> {
return await this.cache.get(key);
}

async setValue(key: string, value: string): Promise<void> {
await this.cache.set(key, value);
}

async delete(key: string): Promise<void> {
await this.cache.del(key);
}

onModuleDestroy() {
(this.cache.store as RedisStore).client.disconnect();
}
}

src/cache/redis-cache.module.ts

import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { ConfigModule } from '@nestjs/config';
import { CacheConfigFactory } from './cache.factory';
import { CacheService } from './cache.service';

@Module({
imports: [
ConfigModule,
CacheModule.registerAsync({
imports: [ConfigModule],
useClass: CacheConfigFactory,
}),
],
providers: [CacheService],
exports: [CacheService, CacheModule],
})
export class RedisCacheModule {}

现在,我们可以在任何 Controller 中使用它来缓存 CacheInterceptor 的请求。

import { CacheInterceptor } from '@nestjs/cache-manager';
...

@Controller('content')
export class ContentController {
constructor(private readonly contentService: ContentService) {}

@UseInterceptors(CacheInterceptor)
@Get('id/:id')
async findOne(
@Param('id', MongoIdTransformer) id: Types.ObjectId,
@Request() request: ProtectedRequest,
): Promise<ContentInfoDto> {
return await this.contentService.findOne(id, request.user);
}

...

}

测试是任何优秀项目的一等公民。该项目广泛地实现了单元测试和集成测试。代码覆盖率超过 75%。

在另一篇文章中,我将详细介绍有效的单元测试和集成测试。同时,您可以通过{feature}.spect.ts文件名来浏览单元测试,例如src/auth/auth.guard.spec.ts。而集成测试则位于test目录内,例如app-auth.e2e-spec.ts。

进行集成测试时,我们会连接到测试数据库。测试所需的配置信息会从.env.test文件中获取。

现在,您可以深入探索这个repo,我相信您会发现它是一个非常有益的练习。

感谢您阅读本文。如果您觉得这篇文章有帮助,请务必分享这篇文章。它会让其他人得到这篇文章并传播知识。此外,您的点赞会激励我写出更多类似的文章。

在janisharali.com上,您可以找到关于我的更多信息。

让我们在TwitterLinkedInGithub上互相关注,成为朋友。

原文链接:https://medium.com/@janishar.ali/mastering-nestjs-building-an-effective-rest-api-backend-8a5add59c2f5

搜索、试用、集成国内外API!
幂简集成API平台已有 4581种API!
API大全
同话题下的热门内容
na
优化利润:计算并报告OpenAI支持的API的COGS
na
API 市场在 5 个领域中的作用