@jsfsi-core / ts-nestjs
用于构建遵循六边形体系结构和领域驱动设计原则的健壮后端应用程序的特定于nestjs的实用程序。
📦安装
npm install @jsfsi-core/ts-nestjs
同行依赖性:
@nestjs/core@nestjs/commonexpressbody-parser
🏗️架构
这个包提供了特定于nestjs的六边形架构模式实现:
- 应用程序启动:已配置的NestJS应用工厂
- 配置:带有Zod验证的类型安全配置服务
- 异常过滤器:在应用程序边缘集中错误处理
- 验证器类型安全的请求验证装饰器
- 中间件):请求日志记录和公共中间件
应用程序结构
src/
├── app/
│ ├── app.ts # Application factory
│ └── bootstrap.ts # Bootstrap helper
├── configuration/
│ └── AppConfigurationService.ts # Configuration setup
├── filters/
│ └── AllExceptionsFilter.ts # Exception handler (edge)
├── guards/
│ └── InMemoryRateLimitGuard.ts # In-memory rate limit guard
├── middlewares/
│ ├── RequestMiddleware.ts # Request logging
│ └── RequestMiddlewareLogCustomizer.ts # Optional custom log payload
└── validators/
└── ZodValidator.ts # Request validators
📋特性
应用程序启动
使用预配置的设置创建类型安全的应用程序:
main.ts:
import 'reflect-metadata';
import * as path from 'path';
import { bootstrap } from '@jsfsi-core/ts-nestjs';
import { GCPLogger } from '@jsfsi-core/ts-nodejs';
import { AppModule } from './app/AppModule';
bootstrap({
appModule: AppModule,
configPath: path.resolve(__dirname, '../configuration'),
logger: new GCPLogger('my-app'),
});
这个 bootstrap 函数:
- 加载指定的环境配置
configPath - 创建并配置NestJS应用程序
- 使用以下命令检索配置
ConfigService与APP_CONFIG_TOKEN - 在配置中指定的端口上自动启动应用程序
- 处理CORS、异常过滤器和日志设置
- 解析JSON请求正文到
JSON_BODY_LIMIT(一个bytes大小如32mb;默认的100kb) - - -createTestingApp应用相同的限制。饰有@JsonBodyLimit('4mb')相反,在其路径下的每个路由上(安装在控制器路径上,因此它假定没有全局前缀;它的解析器不保留),以自己的限制获取主体rawBody)
重要的:你的 AppModule 必须进口 appConfigModuleSetup() 将配置注册到 APP_CONFIG_TOKEN. 如果没有找到配置,bootstrap函数将抛出错误。
配置服务
使用Zod模式的类型安全配置:
import { z } from 'zod';
import { AppConfigSchema, appConfigModuleSetup, APP_CONFIG_TOKEN } from '@jsfsi-core/ts-nestjs';
import { ConfigService } from '@nestjs/config';
// Define configuration schema
export const AppConfigSchema = z.object({
APP_PORT: z
.string()
.transform((val) => parseInt(val, 10))
.refine((val) => !isNaN(val), { message: 'APP_PORT must be a valid number' }),
DATABASE_URL: z.string().url(),
CORS_ORIGIN: z.string().default('*'),
});
export type AppConfig = z.infer<typeof AppConfigSchema>;
// In your app module (AppModule.ts)
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { appConfigModuleSetup, RequestMiddleware } from '@jsfsi-core/ts-nestjs';
import { BrowserAdapter } from '../adapters/BrowserAdapter';
import { HealthController } from '../communication/controllers/health/HealthController';
import { RenderController } from '../communication/controllers/render/RenderController';
import { RenderService } from '../domain/RenderService';
const controllers = [HealthController, RenderController];
const services = [RenderService];
const adapters = [BrowserAdapter];
@Module({
imports: [appConfigModuleSetup()],
controllers: [...controllers],
providers: [...services, ...adapters],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer.apply(RequestMiddleware).forRoutes('*');
}
}
// Use in service
@Injectable()
export class MyService {
constructor(private readonly configService: ConfigService) {}
someMethod() {
const config = this.configService.get<AppConfig>(APP_CONFIG_TOKEN);
// config is fully typed
}
}
异常过滤器
在应用程序边缘集中异常处理:
这个 createApp() 函数自动寄存器 AllExceptionsFilter 的:
- 捕获所有未处理的异常
- 将HTTP异常映射到适当的状态码
- 记录用于监视的错误
- 返回一致的错误响应
注意:这是捕获异常的地方(六边形架构的边缘)。过滤器是自动注册的,不需要手动设置。
请求验证
使用Zod进行类型安全请求验证:
import { Controller, Post } from '@nestjs/common';
import { SafeBody, SafeQuery, SafeParams } from '@jsfsi-core/ts-nestjs';
import { z } from 'zod';
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1),
age: z.number().int().positive(),
});
@Controller('users')
export class UserController {
@Post()
async createUser(@SafeBody(CreateUserSchema) user: z.infer<typeof CreateUserSchema>) {
// user is fully typed based on schema
// Validation happens automatically
// Returns 400 Bad Request if validation fails
}
@Get(':id')
async getUser(@SafeParams(z.object({ id: z.string().uuid() })) params: { id: string }) {
// params.id is validated as UUID
}
@Get()
async listUsers(
@SafeQuery(z.object({ page: z.string().transform(Number).optional() }))
query: {
page?: number;
},
) {
// query.page is validated and transformed
}
}
自定义日志记录器
使用 CustomLogger 而不是内置的NestJS Logger 为了在整个应用程序中保持一致的日志记录:
import { CustomLogger } from '@jsfsi-core/ts-nestjs';
import { Injectable } from '@nestjs/common';
@Injectable()
export class HealthService {
private readonly logger = new CustomLogger(HealthService.name);
async check(): Promise<{ status: string }> {
this.logger.log('Checking health');
return { status: 'OK' };
}
}
这个 CustomLogger NestJS延伸的 Logger 并为整个服务的日志记录提供一致的接口。通过将类名传递给构造函数,日志将以服务名作为前缀,从而更容易在生产中跟踪日志。
可用的方法:
log(message: string)-一般资料error(message: string, trace?: string)-带有可选堆栈跟踪的错误消息warn(message: string)-警告信息debug(message: string)-调试信息(仅在开发中显示)verbose(message: string)-详细日志记录
示例输出:
[Nest] 12345 - 2025/01/01, 12:00:00 LOG [HealthService] Checking health
请求中间件
带有可选自定义有效负载的自动请求日志记录。
基本用法:
import { RequestMiddleware } from '@jsfsi-core/ts-nestjs';
// In your app module
@Module({
// ...
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RequestMiddleware).forRoutes('*');
}
}
基本日志有效载荷 (包括总):
method- HTTP方法url-请求URLstatusCode-响应状态码timeSpentMs—响应时间(毫秒)domain-客户端IP(来自请求)requestHeaders-请求报头responseHeaders-响应头
日志严重性从状态码中选择(例如:2XX→verbose, 4XX→warn, 5XX→error)。
自定义负载 -在每个请求日志中添加额外的字段(例如: userId, tenantId),提供定制器:
- 实现
RequestMiddlewareLogCustomizer与buildLogPayload(req, res): Record<string, unknown>. - 将其注册在
REQUEST_MIDDLEWARE_LOG_CUSTOMIZER模块中的令牌providers.
中间件将自定义有效负载与基本有效负载合并;如果名称冲突,自定义键将覆盖基本键。
import {
REQUEST_MIDDLEWARE_LOG_CUSTOMIZER,
RequestMiddleware,
RequestMiddlewareLogCustomizer,
} from '@jsfsi-core/ts-nestjs';
import { Request, Response } from 'express';
import { Module, NestModule, MiddlewareConsumer, Provider } from '@nestjs/common';
// 1. Implement the customizer
export class RequestUserLogCustomizer implements RequestMiddlewareLogCustomizer {
buildLogPayload(req: Request & { user?: { id: string } }, _res: Response): Record<string, unknown> {
return {
userId: req.user?.id,
};
}
}
const middlewares: Provider[] = [
{
provide: REQUEST_MIDDLEWARE_LOG_CUSTOMIZER,
useClass: RequestUserLogCustomizer,
},
];
@Module({
imports: [appConfigModuleSetup()],
controllers: [...controllers],
providers: [...services, ...adapters, ...middlewares],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer.apply(RequestMiddleware).forRoutes('*');
}
}
使用上面的定制器,每个请求日志将包括基本字段plus userId 当请求具有经过身份验证的用户时。
内存中速率限制保护
InMemoryRateLimitGuard 在滑动时间窗口内限制每个客户端(按IP)的请求数量。它仅在内存中(没有分布式状态),适合单实例api或单节点保护。
选项:
windowMs:以毫秒为单位的时间窗口。600001分钟)。maxRequests:窗口内每个客户端允许的最大请求数。
行为:
- 客户端标识派生自
x-forwarded-for头(第一个IP)或request.ip,否则'unknown'. - 滑动窗口:仅在最近的请求
windowMs数到极限。 - 当超过限制时,警卫就会投掷
HttpStatus.TOO_MANY_REQUESTS(429)并设置Retry-After头(窗口中最老的请求过期前的秒数)。 - 构造函数抛出if
windowMs或者maxRequests不是正的。
用法:
import { InMemoryRateLimitGuard, InMemoryRateLimitGuardOptions } from '@jsfsi-core/ts-nestjs';
import { Controller, Get, UseGuards } from '@nestjs/common';
@Controller('api')
export class ApiController {
@Get('limited')
@UseGuards(new InMemoryRateLimitGuard({ windowMs: 60_000, maxRequests: 100 }))
limited(): { ok: boolean } {
return { ok: true };
}
}
对于应用范围或配置驱动的限制,请扩展 InMemoryRateLimitGuard 并从配置中传递选项(参见template-rest-api README)。
出口: InMemoryRateLimitGuard, InMemoryRateLimitGuardOptions.
📝命名约定
控制器
- 控制器: PascalCase后缀with
Controller(例如,UserController,AuthController) - 端点:使用RESTful命名(例如:
getUser,createUser,updateUser)
服务
- 服务: PascalCase后缀with
Service(例如,UserService,AuthService) - 域名服务:驻留在域层,而不是驻留在NestJS服务中
模块
- 模块: PascalCase后缀with
Module(例如,UserModule,AppModule)
🧪测试原理
测试控制器
import { createTestingApp } from '@jsfsi-core/ts-nestjs';
import { Controller, Get, Module } from '@nestjs/common';
import request from 'supertest';
@Controller('test')
class TestController {
@Get()
getHello(): { message: string } {
return { message: 'Hello' };
}
}
@Module({
controllers: [TestController],
})
class TestModule {}
describe('TestController', () => {
it('returns hello message', async () => {
const app = await createTestingApp(TestModule);
const response = await request(app.getHttpServer()).get('/test');
expect(response.status).toBe(200);
expect(response.body).toEqual({ message: 'Hello' });
});
});
测试服务
import { Test } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
describe('UserService', () => {
let service: UserService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
UserService,
{
provide: ConfigService,
useValue: {
get: jest.fn(),
},
},
],
}).compile();
service = module.get<UserService>(UserService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
使用结果类型进行测试
当服务返回Result类型时,进行相应的测试:
import { isFailure } from '@jsfsi-core/ts-crossplatform';
describe('AuthService', () => {
it('returns user on successful sign in', async () => {
const [user, failure] = await authService.signIn(email, password);
expect(user).toBeDefined();
expect(failure).toBeUndefined();
});
it('returns SignInFailure on authentication error', async () => {
const [user, failure] = await authService.signIn(email, password);
expect(user).toBeUndefined();
expect(isFailure(SignInFailure)(failure)).toBe(true);
});
});
⚠️错误处理原则
边缘异常过滤器
异常应该只在边缘抛出 (在控制器/异常过滤器中),而不是在域逻辑中:
// ✅ Good - In controller (edge)
@Controller('auth')
export class AuthController {
constructor(private readonly authService: AuthenticationService) {}
@Post('signin')
async signIn(@SafeBody(SignInSchema) body: SignInDto) {
const [user, failure] = await this.authService.signIn(body.email, body.password);
if (isFailure(SignInFailure)(failure)) {
throw new UnauthorizedException('Invalid credentials');
}
return user;
}
}
// ✅ Good - Domain service returns Result
export class AuthenticationService {
async signIn(email: string, password: string): Promise<Result<User, SignInFailure>> {
// No exceptions thrown here
return this.authAdapter.signIn(email, password);
}
}
// ✅ Good - Exception filter catches all exceptions
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(error: unknown, host: ArgumentsHost) {
// All exceptions caught here (edge)
}
}
// ❌ Bad - Throwing in domain service
export class AuthenticationService {
async signIn(email: string, password: string): Promise<User> {
// Don't throw exceptions in domain layer
if (!isValid(email)) {
throw new Error('Invalid email');
}
}
}
域内结果类型
域服务应该返回 Result 类型:
// ✅ Good
@Injectable()
export class UserService {
async getUser(id: string): Promise<Result<User, UserNotFoundFailure>> {
const [user, failure] = await this.userRepository.findById(id);
if (isFailure(UserNotFoundFailure)(failure)) {
return Fail(failure);
}
return Ok(user);
}
}
// ✅ Good - Mapping Result to HTTP in controller
@Controller('users')
export class UserController {
@Get(':id')
async getUser(@SafeParams(IdSchema) params: { id: string }) {
const [user, failure] = await this.userService.getUser(params.id);
if (isFailure(UserNotFoundFailure)(failure)) {
throw new NotFoundException('User not found');
}
return user;
}
}
验证错误
使用 SafeBody, SafeQuery, SafeParams 对于自动验证:
// ✅ Good - Automatic validation
@Post('users')
async createUser(@SafeBody(CreateUserSchema) user: CreateUserDto) {
// user is already validated
return this.userService.create(user);
}
// ❌ Bad - Manual validation
@Post('users')
async createUser(@Body() user: any) {
// Manual validation needed
if (!user.email) {
throw new BadRequestException('Email required');
}
}
🎯领域驱动设计
域层结构
领域逻辑应该是框架不可知的:
src/
├── domain/
│ ├── models/
│ │ ├── User.ts
│ │ └── SignInFailure.ts
│ └── services/
│ └── AuthenticationService.ts # Domain service (no NestJS dependencies)
├── adapters/
│ └── DatabaseAdapter.ts # Implements domain interfaces
└── controllers/ # NestJS-specific (edge)
└── AuthController.ts
域名服务
域服务包含业务逻辑:
// ✅ Good - Domain service (no NestJS decorators)
export class AuthenticationService {
constructor(private readonly authAdapter: AuthenticationAdapter) {}
async signIn(email: string, password: string): Promise<Result<User, SignInFailure>> {
// Business logic here
return this.authAdapter.signIn(email, password);
}
}
// ✅ Good - Inject domain service in NestJS service
@Injectable()
export class AuthService {
constructor(private readonly authenticationService: AuthenticationService) {}
async signIn(email: string, password: string) {
return this.authenticationService.signIn(email, password);
}
}
🔄结果类集成
使用结果类型
域服务返回结果类型,控制器映射到HTTP:
import { Result, isFailure } from '@jsfsi-core/ts-crossplatform';
@Controller('orders')
export class OrderController {
constructor(private readonly orderService: OrderService) {}
@Post()
async createOrder(@SafeBody(CreateOrderSchema) order: CreateOrderDto) {
const [orderId, failure] = await this.orderService.create(order);
if (isFailure(ValidationFailure)(failure)) {
throw new BadRequestException(failure.message);
}
if (isFailure(PaymentFailure)(failure)) {
throw new PaymentRequiredException('Payment failed');
}
return { id: orderId };
}
}
误差映射
将域故障映射为HTTP异常:
function mapFailureToHttpException(failure: Failure): HttpException {
if (isFailure(ValidationFailure)(failure)) {
return new BadRequestException(failure.message);
}
if (isFailure(NotFoundFailure)(failure)) {
return new NotFoundException(failure.message);
}
if (isFailure(UnauthorizedFailure)(failure)) {
return new UnauthorizedException(failure.message);
}
return new InternalServerErrorException('An error occurred');
}
📚最佳实践
1. 依赖注入
使用构造函数注入:
@Injectable()
export class UserService {
constructor(
private readonly userRepository: UserRepository,
private readonly configService: ConfigService,
) {}
}
2. 模块组织
将模块中的相关功能分组:
@Module({
imports: [TypeOrmModule.forFeature([UserEntity])],
controllers: [UserController],
providers: [UserService, UserRepository],
exports: [UserService],
})
export class UserModule {}
3. 配置
始终使用类型化配置:
// ✅ Good
const config = this.configService.get<AppConfig>(APP_CONFIG_TOKEN);
// ❌ Bad
const port = process.env.PORT; // Not type-safe
4. 请求验证
总是用Zod模式验证请求:
// ✅ Good
@Post()
async create(@SafeBody(CreateSchema) data: CreateDto) {
// data is validated and typed
}
// ❌ Bad
@Post()
async create(@Body() data: any) {
// No validation, no type safety
}
5. 错误处理
在域中使用结果类型,只在边缘使用例外:
// Domain: Result types
async getUser(id: string): Promise<Result<User, UserNotFoundFailure>> {
// ...
}
// Controller: Map to HTTP
async getUser(@Param('id') id: string) {
const [user, failure] = await this.service.getUser(id);
if (isFailure(UserNotFoundFailure)(failure)) {
throw new NotFoundException();
}
return user;
}
🔗其他资源
NestJS
体系结构
验证
📄许可证
国际安全委员会