@pawells/nestjs-shared
v3.0.5
Published
Shared NestJS infrastructure: filters, logging, metrics, and security
Readme
NestJS Shared Module
Foundational NestJS infrastructure library providing filters, guards, interceptors, logging, CSRF protection, error handling, configuration, metrics, and lazy loading utilities.
Installation
yarn add @pawells/nestjs-sharedRequirements
- Node.js: >= 22.0.0
- NestJS: >= 10.0.0
Peer Dependencies
{
"@nestjs/common": ">=10.0.0",
"@nestjs/config": ">=3.0.0",
"@nestjs/core": ">=10.0.0",
"@nestjs/throttler": ">=5.0.0",
"@opentelemetry/api": ">=1.0.0",
"class-transformer": ">=0.5.0",
"class-validator": ">=0.14.0",
"compression": ">=1.0.0",
"csrf-csrf": ">=4.0.0",
"express": ">=5.0.0",
"helmet": ">=7.0.0",
"joi": ">=18.0.0",
"prom-client": ">=15.0.0",
"rxjs": ">=7.0.0",
"xss": ">=1.0.0"
}Quick Start
1. Initialize ConfigModule (MUST come first)
import { Module } from '@nestjs/common';
import { ConfigModule } from '@pawells/nestjs-shared';
@Module({
imports: [
ConfigModule, // Must be first
],
})
export class AppModule {}2. Import CommonModule
import { Module } from '@nestjs/common';
import { CommonModule } from '@pawells/nestjs-shared';
@Module({
imports: [
ConfigModule, // Must be first
CommonModule, // Depends on ConfigModule
],
})
export class AppModule {}3. Use Global Filters and Pipes
Global filters and pipes are automatically registered by CommonModule:
- GlobalExceptionFilter & HttpExceptionFilter: Standardized error responses
- ValidationPipe: Automatic DTO validation
All are applied globally. For HTTP metrics collection, also import MetricsModule.ForRoot().
Features
Error Handling
Comprehensive error handling with structured responses, categorization, and sanitization.
import {
BaseApplicationError,
GlobalExceptionFilter,
ErrorCategorizerService,
ErrorSanitizerService,
} from '@pawells/nestjs-shared';
// Create custom error
export class UserNotFoundError extends BaseApplicationError {
constructor(userId: string) {
super(`User ${userId} not found`, {
code: 'USER_NOT_FOUND',
statusCode: 404,
context: { userId }
});
}
}
// Throw it
throw new UserNotFoundError('123');
// Automatically caught by GlobalExceptionFilter
// Response includes code, message, timestamp, sanitized context, stack (dev only)Error Categories: Errors are automatically categorized as transient (retryable) or permanent (fail-fast) with recommended recovery strategies.
Logging
Structured logging with OpenTelemetry trace context injection, backed by @pawells/logger.
NestJSLogger (DI pattern — for services, controllers, guards)
Use constructor injection for any component registered in the NestJS DI container:
import { Injectable } from '@nestjs/common';
import { NestJSLogger } from '@pawells/nestjs-shared';
@Injectable()
export class UserService {
private readonly Logger: NestJSLogger;
constructor(logger: NestJSLogger) {
this.Logger = logger;
}
public CreateUser(): void {
this.Logger.log('User created', 'UserService');
this.Logger.error('Database error', error.stack, 'UserService');
this.Logger.warn('Rate limit approaching', 'UserService');
this.Logger.debug('Cache hit', 'UserService');
}
}Static Logger pattern (for utility classes and standalone functions)
Use @pawells/logger directly for classes that are not part of the DI container:
import { Logger } from '@pawells/logger';
export class UtilityClass {
private static readonly Logger = new Logger('UtilityClass');
public static DoSomething(): void {
UtilityClass.Logger.info('Doing something');
UtilityClass.Logger.error('Error', { context: 'processing' });
}
}Bootstrap setup
Configure NestJS to use NestJSLogger as the application logger in main.ts:
import { NestFactory } from '@nestjs/core';
import { NestJSLogger } from '@pawells/nestjs-shared';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
bufferLogs: true,
});
app.useLogger(new NestJSLogger());
await app.listen(process.env['PORT'] ?? 3000);
}
bootstrap();Logging methods
All methods accept message + optional context (last parameter):
logger.log(message, context?)— Info levellogger.debug(message, context?)— Debug levellogger.warn(message, context?)— Warning levellogger.error(message, stack?, context?)— Error level (special stack param)logger.verbose(message, context?)— Verbose level (mapped to debug)logger.fatal(message, context?)— Fatal level
See AGENTS.md for the complete logging pattern reference including LogManager coordination.
Log Levels: Controlled by LOG_LEVEL environment variable (debug, info, warn, error, fatal, silent).
Metrics
Metrics collection via InstrumentationRegistry. HTTP metrics and the @Instrument() decorator require MetricsModule.ForRoot() — they are not part of CommonModule.
import { Module } from '@nestjs/common';
import { CommonModule, MetricsModule } from '@pawells/nestjs-shared';
@Module({
imports: [
CommonModule,
MetricsModule.ForRoot(), // Required for HTTP metrics and @Instrument() support
],
})
export class AppModule {}InstrumentationRegistry is the central registry for metric collection and export. It is provided by MetricsModule and injectable once that module is imported:
import { Injectable } from '@nestjs/common';
import { InstrumentationRegistry } from '@pawells/nestjs-shared';
@Injectable()
export class OrderService {
private readonly Registry: InstrumentationRegistry;
constructor(registry: InstrumentationRegistry) {
this.Registry = registry;
}
public Initialize(): void {
this.Registry.RegisterDescriptor({
name: 'orders_total',
type: 'counter',
help: 'Total orders processed',
labelNames: ['status'],
});
}
public RecordOrder(status: string): void {
this.Registry.RecordMetric('orders_total', 1, { status });
}
}Features:
- Automatic HTTP request metrics (duration histogram, request counter, size histogram) via
HTTPMetricsInterceptorinMetricsModule - Dynamic path normalization (UUIDs, ObjectIDs, numeric IDs →
:id) to prevent unbounded cardinality @Instrument()decorator for automatic method timing, counters, and error tracking- Event-based and pull-based exporter support
- Prometheus export via
@pawells/nestjs-prometheus
CSRF Protection
Double-Submit Cookie pattern with per-IP rate limiting.
import { CSRFGuard, CSRFService } from '@pawells/nestjs-shared';
// Globally applied by CommonModule
// Or manually on specific controller
@UseGuards(CSRFGuard)
@Controller('api')
export class ApiController {
constructor(private csrf: CSRFService) {}
@Post('/form')
async submitForm(@Req() req: Request, @Res() res: Response) {
// Generate token
const token = await this.csrf.GenerateToken(req, res);
res.render('form', { csrfToken: token });
}
@Post('/process')
async processForm(@Req() req: Request) {
// Validation done automatically by CSRFGuard
// Safe methods (GET, HEAD, OPTIONS) bypass validation
}
}Features:
- Token generation with rate limiting (10 per IP per 60s)
- Session binding or IP-based fallback
- Automatic pruning of stale timestamps
- Capacity monitoring with safety margins
- Configurable proxy trust for X-Forwarded-For header
- CSRF_SECRET entropy validation at startup
Validation
Automatic DTO validation and transformation.
import { IsEmail, IsNotEmpty, MinLength } from 'class-validator';
export class CreateUserDto {
@IsNotEmpty()
@IsEmail()
email: string;
@IsNotEmpty()
@MinLength(8)
password: string;
}
@Post()
async createUser(@Body() dto: CreateUserDto) {
// DTO automatically validated and transformed
// Validation errors formatted as error array with field paths
}Features:
- Automatic DTO validation via ValidationPipe
- Nested object support with path prefixes
- Comprehensive error formatting
- Class transformation with class-transformer
Health Checks
Kubernetes-ready health, readiness, and liveness probes.
import { HealthCheckService, HealthStatus } from '@pawells/nestjs-shared';
constructor(private health: HealthCheckService) {}
@Get('/health')
getHealth() {
return this.health.GetHealth('my-service', '1.0.0');
}
@Get('/ready')
getReadiness() {
return this.health.GetReadiness({
database: HealthStatus.OK,
cache: HealthStatus.OK,
});
}
@Get('/live')
getLiveness() {
return this.health.GetLiveness();
}Configuration
Type-safe environment variable access with validation.
import { ConfigService } from '@pawells/nestjs-shared';
constructor(private config: ConfigService) {}
// Type-safe getters
const port = this.config.GetNumber('PORT') ?? 3000;
const nodeEnv = this.config.GetString('NODE_ENV') ?? 'development';
const dbUrl = this.config.GetOrThrow('DATABASE_URL');
// Validation
this.config.Validate({
PORT: { required: true },
DATABASE_URL: { required: true },
LOG_LEVEL: { required: false },
});Lazy Loading
Defer dependency resolution to avoid circular dependencies.
Factory functions follow PascalCase naming per project conventions.
import {
TLazyGetter,
ILazyModuleRefService,
CreateMemoizedLazyGetter,
} from '@pawells/nestjs-shared';
import { ModuleRef } from '@nestjs/core';
@Injectable()
export class MyService implements ILazyModuleRefService {
private readonly UserServiceGetter: TLazyGetter<UserService>;
public readonly Module: ModuleRef;
constructor(module: ModuleRef) {
this.Module = module;
this.UserServiceGetter = CreateMemoizedLazyGetter(
() => this.Module.get(UserService, { strict: false }),
);
}
public async GetUser(id: string) {
// UserService resolved lazily on first call
return this.UserServiceGetter().getById(id);
}
}Features:
TLazyGetter<T>: Type alias for a required dependency getter functionTOptionalLazyGetter<T>: Type alias for an optional dependency getter functionCreateMemoizedLazyGetter: Factory that caches the resolved dependency after first callCreateOptionalLazyGetter: Safe resolution that returnsundefinedinstead of throwingILazyModuleRefService: Interface requiring a publicModule: ModuleRefproperty
HTTP Client
Robust HTTP client with timeout, SSL/TLS, and sensitive data redaction.
import { HttpClientService } from '@pawells/nestjs-shared';
constructor(private http: HttpClientService) {}
// GET request
const response = await this.http.Get<User>('https://api.example.com/users/123');
// POST request with custom timeout
const response = await this.http.Post<User>(
'https://api.example.com/users',
{ name: 'John', email: '[email protected]' },
{ timeout: 5000, correlationId: 'req-123' }
);
// HTTPS with custom CA certificate
const cert = fs.readFileSync('/path/to/ca.pem');
const response = await this.http.Get(
'https://internal-api.local/data',
{ ca: cert }
);Features:
- Configurable timeouts (default: HTTP_CLIENT_TIMEOUT)
- SSL/TLS certificate validation (strict by default)
- Custom CA certificate support
- Payload size limit (10MB)
- Automatic content-type parsing
- Correlation ID support
- Sensitive data redaction in logs
- Duration tracking
Decorators
Request property extractors for cleaner controller methods.
import { Query, Params, Body, Headers, Cookies } from '@pawells/nestjs-shared';
@Controller('users')
export class UserController {
@Get(':id')
getUser(
@Params('id') id: string,
@Query('include') include?: string,
) {
// Route params and query params extracted
}
@Post()
createUser(
@Body() dto: CreateUserDto,
@Headers('authorization') auth?: string,
) {
// Body and headers extracted
}
@Get()
listUsers(@Cookies('sessionId') sessionId?: string) {
// Cookies extracted
}
}Core Modules
CommonModule
Global module providing foundational infrastructure. Does NOT include metrics — import MetricsModule.ForRoot() separately for HTTP metrics and @Instrument() decorator support.
// Automatically applied globally
@Global()
@Module({
providers: [
// Filters (global)
GlobalExceptionFilter,
HttpExceptionFilter,
// Pipe (global)
ValidationPipe,
// Services (injectable)
NestJSLogger,
CSRFService,
CSRFGuard,
ErrorCategorizerService,
ErrorSanitizerService,
HealthCheckService,
],
exports: [...],
})
export class CommonModule {}ConfigModule
Configuration service with validation.
@Module({
imports: [ConfigModule],
// ConfigService and ValidationService automatically available
})
export class AppModule {}MetricsModule
Optional module that enables HTTP metrics collection and the @Instrument() decorator. Import it explicitly in any application that needs metrics.
@Module({
imports: [
CommonModule,
MetricsModule.ForRoot(), // Opt-in: HTTP metrics + @Instrument() support
],
})
export class AppModule {}
// Provides:
// - InstrumentationRegistry (injectable)
// - HTTPMetricsInterceptor (registered as global APP_INTERCEPTOR)
// - @Instrument() decorator supportAPI Reference
Services
NestJSLogger
NestJS-compatible logger that extends ConsoleLogger and feeds entries into @pawells/logger LogManager. Provided by CommonModule and injectable via constructor injection.
Methods:
log(message, context?): Info leveldebug(message, context?): Debug levelwarn(message, context?): Warning levelerror(message, stack?, context?): Error level with optional stack traceverbose(message, context?): Verbose level (mapped to debug)fatal(message, context?): Fatal levelCreateContextualLogger(context): Create a newNestJSLoggerinstance bound to a specific context string
Pass new NestJSLogger() to app.useLogger() in main.ts to route all NestJS framework logs through @pawells/logger. See the Logging section above for the full bootstrap and DI patterns.
ErrorCategorizerService
Classifies errors as transient/permanent and recommends recovery strategy.
Methods:
CategorizeError(error): ReturnsIErrorCategorywithtype,retryable,strategy,backoffMsIsRetryable(error): Boolean checkLogRecoveryAttempt(error, attempt, maxAttempts): Log retry attemptLogRecoverySuccess(error, attempts): Log successful recoveryLogRecoveryFailed(error, attempts): Log failed recovery
ErrorSanitizerService
Removes sensitive information from error responses before they are sent to clients.
Methods:
SanitizeErrorResponse(error, isDevelopment?): Sanitize error object for client response; strips stack traces in production and redacts sensitive field values
Redactions applied to messages: file paths, database URIs, API keys, Bearer tokens, email addresses, IP addresses. Redactions applied to context objects: field names matching sensitive patterns (password, token, secret, key, etc.).
CSRFService
CSRF token generation and validation with rate limiting.
Methods:
GenerateToken(req, res): Generate and set CSRF token; rate-limited to 10 per IP per 60 secondsValidateToken(req): Validate CSRF token; returnstrueif validRefreshToken(req, res): Generate a fresh token after sensitive operations (login, password change)GetMiddleware(): Return the underlyingdoubleCsrfmiddleware function
InstrumentationRegistry
Central registry for metrics collection and export. Provided by MetricsModule — requires MetricsModule.ForRoot() to be imported.
Methods:
RegisterDescriptor(descriptor): Register a metric descriptor (name, type, help, labelNames, buckets, unit); idempotent on exact match, throws on conflictRecordMetric(name, value, labels?): Record a numeric value for a registered metric; emits to all exporters and named listenersGetAllMetrics(): Return a copy of all recorded metric values as aMap<string, IMetricValue[]>GetMetric(name): Return recorded values for a single metric by nameOn(metricName, handler): Subscribe to a metric by name; returns an unsubscribe functionRegisterExporter(exporter): Register a metrics exporter (e.g., Prometheus, OpenTelemetry)Shutdown(): Flush and shut down all registered exporters
HealthCheckService
Kubernetes health probes.
Methods:
GetHealth(serviceName?, version?): General health checkGetReadiness(checks?): Readiness probe (can the service receive traffic)GetLiveness(): Liveness probe (is the service alive)
HttpClientService
HTTP client with timeout and SSL/TLS support.
Methods:
Request<T>(options): Make an HTTP request with full options controlGet<T>(url, options?): GET requestPost<T>(url, data?, options?): POST requestPut<T>(url, data?, options?): PUT requestDelete<T>(url, options?): DELETE request
ConfigService
Type-safe environment variable access.
Methods:
Get<T>(propertyPath, defaultValue?): Return config value or defaultGetOrThrow<T>(propertyPath): Return config value or throw if not setGetString(propertyPath, defaultValue?): Return value coerced to stringGetNumber(propertyPath, defaultValue?): Return value coerced to numberValidate(schema): Validate required fields against schema; throws on missing required keys
Filters
GlobalExceptionFilter
Catches unhandled exceptions (except HttpException).
- Standardizes response format
- Sanitizes sensitive data
- Categorizes errors
- Logs with context
HttpExceptionFilter
Handles HTTP exceptions.
- Formats NestJS built-in exceptions
- Sanitizes responses
- Categorizes for logging
Interceptors
LoggingInterceptor
Logs incoming requests and outgoing responses.
- Uses DEBUG level for /health and /metrics
- Uses INFO level for other requests
- Includes method, URL, IP, duration
HTTPMetricsInterceptor
Collects HTTP request metrics.
- Duration histogram
- Request counter
- Request size histogram
- Automatic route normalization
Guards
CSRFGuard
Validates CSRF tokens on state-changing requests.
- Bypasses safe methods (GET, HEAD, OPTIONS)
- Enforces validation for POST/PUT/DELETE/PATCH
- Throws 403 on validation failure
- Logs violations to audit log
Pipes
ValidationPipe
Validates DTOs using class-validator.
- Automatic transformation via class-transformer
- Nested object support with path prefixes
- Comprehensive error formatting
Interfaces & Types
ILogger
Basic logging interface without contextual logger creation.
IContextualLogger
Extended logging with contextual logger creation.
ILazyModuleRefService
Interface for services using the lazy ModuleRef pattern.
interface ILazyModuleRefService {
Module: ModuleRef;
}TLazyGetter
Function type for a getter that returns a required dependency.
type TLazyGetter<T> = () => T;TOptionalLazyGetter
Function type for a getter that returns a dependency or undefined.
type TOptionalLazyGetter<T> = () => T | undefined;IErrorCategory
Error classification result from ErrorCategorizerService.CategorizeError.
interface IErrorCategory {
type: 'transient' | 'permanent';
retryable: boolean;
strategy: 'retry' | 'fail' | 'backoff';
backoffMs?: number;
}IHealthCheck
Health check response structure.
interface IHealthCheck {
status: string;
timestamp: string;
service?: string;
version?: string;
checks?: Record<string, string>;
}Conditional Exports
// Main exports (all major classes and utilities)
import { NestJSLogger, CSRFService } from '@pawells/nestjs-shared';
// Common-only exports (lower-level utilities)
import { CSRFService } from '@pawells/nestjs-shared/common';
// Lazy loader types only
import { TLazyGetter } from '@pawells/nestjs-shared/common/utils/lazy-getter.types';
// Testing utilities
import { AppLoggerMock, SharedTestingModule } from '@pawells/nestjs-shared/testing';AppLoggerMock is a no-op logger available from the /testing subpath. It provides silent implementations of all NestJSLogger-compatible methods and can be used as a provider substitute in unit tests. For full spy behaviour, wrap individual methods with vi.fn() after obtaining the instance, or use vi.mock() to mock NestJSLogger directly.
Configuration
Environment Variables
- NODE_ENV: development, production, etc. (affects error details, logging)
- LOG_LEVEL: debug, info, warn, error, fatal, silent (default: info)
- PORT: Server port (default: 3000)
- CSRF_SECRET: Required for CSRF protection (min 32 chars, high entropy)
- METRICS_ENABLED: Enable metrics (default: true)
- SERVICE_NAME: Service name for logging (default: unknown-service)
CSRF_SECRET Generation
# Generate secure CSRF_SECRET
openssl rand -hex 32Security Defaults
- CSRF Token Generation: Per-IP rate limited (10 per 60s)
- CSRF Validation: Signed tokens with session/IP binding
- Error Sanitization: Stack traces only in development; sensitive fields redacted
- CORS: Implemented via security bootstrap (if configured)
- HTTP Client: Strict SSL/TLS certificate validation (rejectUnauthorized: true)
- Metrics: Dynamic path normalization prevents unbounded cardinality
- Logging: Automatic redaction of passwords, tokens, API keys, emails, IPs
Examples
Complete Application Setup
import { NestFactory } from '@nestjs/core';
import { ConfigModule, CommonModule, MetricsModule } from '@pawells/nestjs-shared';
@Module({
imports: [
ConfigModule, // MUST be first
CommonModule, // Depends on ConfigModule
MetricsModule.ForRoot(), // Optional: Prometheus metrics
// ... feature modules
],
})
export class AppModule {}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// All filters, interceptors, pipes already registered globally
const port = process.env['PORT'] ?? 3000;
await app.listen(port);
}
bootstrap();Custom Error Handling
import { BaseApplicationError, ErrorCategorizerService } from '@pawells/nestjs-shared';
export class InsufficientFundsError extends BaseApplicationError {
constructor(required: number, available: number) {
super(`Insufficient funds: need ${required}, have ${available}`, {
code: 'INSUFFICIENT_FUNDS',
statusCode: 402,
context: { required, available }
});
}
}
// In service
@Catch(InsufficientFundsError)
export class PaymentService {
constructor(private errorCategorizer: ErrorCategorizerService) {}
async processPayment(amount: number) {
if (amount > balance) {
const error = new InsufficientFundsError(amount, balance);
// Automatically caught by GlobalExceptionFilter
// Categorized by ErrorCategorizerService (permanent/fail)
// Sanitized by ErrorSanitizerService
throw error;
}
}
}Related Packages
- @pawells/nestjs-auth - Keycloak integration: token validation, guards, decorators, Admin API client
- @pawells/nestjs-open-telemetry - OpenTelemetry tracing
- @pawells/nestjs-prometheus - Prometheus metrics exporter
License
MIT
