npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@jsfsi-core/ts-nestjs

v1.1.24

Published

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

Readme

@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)
├── middlewares/
│   └── RequestMiddleware.ts       # Request logging
└── 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

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:

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

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

Logs include:

  • HTTP method and URL
  • Status code
  • Response time
  • Request/response headers
  • Severity level based on status code

📝 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