@jsfsi-core/ts-nestjs

中文文档
2.0.10发布于 2026/11/01ISCTypeScriptESM + CJS构建来源已验证

NestJS-specific utilities for building robust backend applications following hexagonal architecture and domain-driven design principles.

原始使用文档
文档目录40 节
  1. 📦 Installation
  2. 🏗️ Architecture
  3. Application Structure
  4. 📋 Features
  5. Application Bootstrap
  6. Configuration Service
  7. Exception Filter
  8. Request Validation
  9. Custom Logger
  10. Request Middleware
  11. In-Memory Rate Limit Guard
  12. 📝 Naming Conventions
  13. Controllers
  14. Services
  15. Modules
  16. 🧪 Testing Principles
  17. Testing Controllers
  18. Testing Services
  19. Testing with Result Types
  20. ⚠️ Error Handling Principles
  21. Exception Filter at Edge
  22. Result Types in Domain
  23. Validation Errors
  24. 🎯 Domain-Driven Design
  25. Domain Layer Structure
  26. Domain Services
  27. 🔄 Result Class Integration
  28. Using Result Types
  29. Error Mapping
  30. 📚 Best Practices
  31. 1. Dependency Injection
  32. 2. Module Organization
  33. 3. Configuration
  34. 4. Request Validation
  35. 5. Error Handling
  36. 🔗 Additional Resources
  37. NestJS
  38. Architecture
  39. Validation
  40. 📄 License

@jsfsi-core/ts-nestjs

NestJS-specific utilities for building robust backend applications following hexagonal architecture and domain-driven design principles.

📦 Installation

npm install @jsfsi-core/ts-nestjs

Peer Dependencies:

  • @nestjs/core
  • @nestjs/common
  • express
  • body-parser

🏗️ Architecture

This package provides NestJS-specific implementations of hexagonal architecture patterns:

  • Application Bootstrap: Configured NestJS application factory
  • Configuration: Type-safe configuration service with Zod validation
  • Exception Filters: Centralized error handling at application edges
  • Validators: Type-safe request validation decorators
  • Middlewares: Request logging and common middleware

Application Structure

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

📋 Features

Application Bootstrap

Type-safe application creation with pre-configured settings:

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'),
});

The bootstrap function:

  • Loads environment configuration from the specified configPath
  • Creates and configures the NestJS application
  • Retrieves configuration using ConfigService with APP_CONFIG_TOKEN
  • Automatically starts the application on the port specified in your configuration
  • Handles CORS, exception filters, and logging setup
  • Parses JSON request bodies up to JSON_BODY_LIMIT (a bytes size such as 32mb; default 100kb) — createTestingApp applies the same limit. A controller decorated with @JsonBodyLimit('4mb') takes bodies up to its own limit instead, on every route under its path (mounted at the controller path, so it assumes no global prefix; its parser does not keep rawBody)

Important: Your AppModule must import appConfigModuleSetup() to register the configuration with the APP_CONFIG_TOKEN. The bootstrap function will throw an error if the configuration is not found.

Configuration Service

Type-safe configuration with Zod schemas:

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
  }
}

Exception Filter

Centralized exception handling at the application edge:

The createApp() function automatically registers AllExceptionsFilter which:

  • Catches all unhandled exceptions
  • Maps HTTP exceptions to appropriate status codes
  • Logs errors for monitoring
  • Returns consistent error responses

Note: This is where exceptions are caught (edge of hexagonal architecture). The filter is automatically registered, no manual setup needed.

Request Validation

Type-safe request validation with 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
  }
}

Custom Logger

Use CustomLogger instead of the built-in NestJS Logger for consistent logging across your application:

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' };
  }
}

The CustomLogger extends NestJS's Logger and provides a consistent interface for logging throughout your services. By passing the class name to the constructor, logs will be prefixed with the service name, making it easier to trace logs in production.

Available methods:

  • log(message: string) - General information
  • error(message: string, trace?: string) - Error messages with optional stack trace
  • warn(message: string) - Warning messages
  • debug(message: string) - Debug information (only shown in development)
  • verbose(message: string) - Verbose logging

Example output:

[Nest] 12345  - 2025/01/01, 12:00:00     LOG [HealthService] Checking health

Request Middleware

Automatic request logging with an optional custom payload.

Basic usage:

import { RequestMiddleware } from '@jsfsi-core/ts-nestjs';

// In your app module
@Module({
  // ...
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(RequestMiddleware).forRoutes('*');
  }
}

Base log payload (always included):

  • method – HTTP method
  • url – Request URL
  • statusCode – Response status code
  • timeSpentMs – Response time in milliseconds
  • domain – Client IP (from request)
  • requestHeaders – Request headers
  • responseHeaders – Response headers

Log severity is chosen from the status code (e.g. 2XX → verbose, 4XX → warn, 5XX → error).

Custom payload – To add extra fields to each request log (e.g. userId, tenantId), provide a customizer:

  1. Implement RequestMiddlewareLogCustomizer with buildLogPayload(req, res): Record<string, unknown>.
  2. Register it under the REQUEST_MIDDLEWARE_LOG_CUSTOMIZER token in your module providers.

The middleware merges the custom payload with the base payload; customizer keys override base keys if names collide.

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('*');
  }
}

With the customizer above, each request log will include the base fields plus userId when the request has an authenticated user.

In-Memory Rate Limit Guard

InMemoryRateLimitGuard limits the number of requests per client (by IP) within a sliding time window. It is in-memory only (no distributed state) and suitable for single-instance APIs or per-node protection.

Options:

  • windowMs: Time window in milliseconds (e.g. 60000 for 1 minute).
  • maxRequests: Maximum number of requests allowed per client within the window.

Behaviour:

  • Client identity is derived from the x-forwarded-for header (first IP) or request.ip, otherwise 'unknown'.
  • Sliding window: only requests within the last windowMs ms count toward the limit.
  • When the limit is exceeded, the guard throws HttpStatus.TOO_MANY_REQUESTS (429) and sets the Retry-After header (seconds until the oldest request in the window expires).
  • The constructor throws if windowMs or maxRequests is not positive.

Usage:

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 };
  }
}

For app-wide or config-driven limits, extend InMemoryRateLimitGuard in your application and pass options from your configuration (see the template-rest-api README).

Exports: InMemoryRateLimitGuard, InMemoryRateLimitGuardOptions.

📝 Naming Conventions

Controllers

  • Controllers: PascalCase suffix with Controller (e.g., UserController, AuthController)
  • Endpoints: Use RESTful naming (e.g., getUser, createUser, updateUser)

Services

  • Services: PascalCase suffix with Service (e.g., UserService, AuthService)
  • Domain Services: Live in domain layer, not in NestJS services

Modules

  • Modules: PascalCase suffix with Module (e.g., UserModule, AppModule)

🧪 Testing Principles

Testing Controllers

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' });
  });
});

Testing Services

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();
  });
});

Testing with Result Types

When services return Result types, test accordingly:

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);
  });
});

⚠️ Error Handling Principles

Exception Filter at Edge

Exceptions should only be thrown at the edge (in controllers/exception filters), not in domain logic:

// ✅ 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 Types in Domain

Domain services should return Result types:

// ✅ 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;
  }
}

Validation Errors

Use SafeBody, SafeQuery, SafeParams for automatic validation:

// ✅ 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');
  }
}

🎯 Domain-Driven Design

Domain Layer Structure

Domain logic should be framework-agnostic:

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

Domain Services

Domain services contain business logic:

// ✅ 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);
  }
}

🔄 Result Class Integration

Using Result Types

Domain services return Result types, controllers map to 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 };
  }
}

Error Mapping

Map domain failures to HTTP exceptions:

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');
}

📚 Best Practices

1. Dependency Injection

Use constructor injection:

@Injectable()
export class UserService {
  constructor(
    private readonly userRepository: UserRepository,
    private readonly configService: ConfigService,
  ) {}
}

2. Module Organization

Group related functionality in modules:

@Module({
  imports: [TypeOrmModule.forFeature([UserEntity])],
  controllers: [UserController],
  providers: [UserService, UserRepository],
  exports: [UserService],
})
export class UserModule {}

3. Configuration

Always use typed configuration:

// ✅ Good
const config = this.configService.get<AppConfig>(APP_CONFIG_TOKEN);

// ❌ Bad
const port = process.env.PORT; // Not type-safe

4. Request Validation

Always validate requests with Zod schemas:

// ✅ 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. Error Handling

Use Result types in domain, exceptions only at edge:

// 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;
}

🔗 Additional Resources

NestJS

Architecture

Validation

📄 License

ISC

版本与发布记录

@jsfsi-core/ts-nestjs 共发布 57 个版本,首次发布于 2025/10/29;过去 12 个月发布了 46 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
2.0.10latest2026/11/012 天前
2.0.92026/11/012 天前
2.0.82026/10/304 天前
2.0.72026/10/304 天前
2.0.62026/08/282 个月前
2.0.52026/08/272 个月前

关于 @jsfsi-core/ts-nestjs 的常见问题

@jsfsi-core/ts-nestjs 是什么?

用于构建遵循六边形体系结构和领域驱动设计原则的健壮后端应用程序的特定于nestjs的实用程序。

如何安装 @jsfsi-core/ts-nestjs?

在项目目录运行 npm install @jsfsi-core/ts-nestjs,或使用 pnpm add / yarn add / bun add。

@jsfsi-core/ts-nestjs 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@jsfsi-core/ts-nestjs 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@jsfsi-core/ts-nestjs 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js 26.7.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@jsfsi-core/ts-nestjs 可以商用吗?

@jsfsi-core/ts-nestjs 使用 ISC 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@jsfsi-core/ts-nestjs 还在维护吗?

最新版本 2.0.10 发布于 2026/11/01(2 天前);至今共发布 57 个版本,其中过去 12 个月发布了 46 个;首次发布于 2025/10/29。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。