nest-common

@tsdevstack/nest-common is the shared NestJS library for tsdevstack projects. It provides authentication, secrets management, Redis, observability, rate limiting, notifications, background jobs, service-to-service communication, and database utilities — all configured to work across GCP, AWS, and Azure.

npm install @tsdevstack/nest-common

Pre-installed in all NestJS service templates — no separate setup needed.

Quick start

A typical AppModule imports the modules you need:

import { Module } from '@nestjs/common';
import {
  ObservabilityModule,
  SecretsModule,
  AuthModule,
  RedisModule,
  BullConfigModule,
  NotificationModule,
  StorageModule,
  MessagingModule,
} from '@tsdevstack/nest-common';
import { BullModule } from '@nestjs/bullmq';

@Module({
  imports: [
    ObservabilityModule,                        // Logging, metrics, tracing, health
    SecretsModule,                               // Multi-provider secret access
    AuthModule,                                  // JWT + API key auth
    RedisModule,                                 // Redis client
    BullConfigModule.forRoot(),                  // BullMQ with Redis
    BullModule.registerQueue({ name: 'emails' }),
    NotificationModule,                          // Email notifications
    StorageModule.forRoot({ buckets: ['uploads'] }), // Object storage
    MessagingModule.forRoot({ consumerGroup: 'my-service', topics: ['user-created'] }), // Async messaging
  ],
})
export class AppModule {}

All modules are global — import once in AppModule and they're available everywhere.

Modules overview

ModuleWhat it does
ObservabilityModuleStructured logging, Prometheus metrics, distributed tracing, health checks
SecretsModuleSecret access across local, GCP, AWS, Azure
AuthModuleJWT and API key authentication via Kong gateway
RedisModuleRedis client with TLS support
BullConfigModuleBullMQ configuration with Redis from secrets
NotificationModuleEmail notifications (console in dev, Resend in prod)
RateLimitModuleRedis-backed rate limiting
EmailRateLimitModulePer-email rate limiting
StorageModuleUnified object storage (S3, GCS, Azure Blob, MinIO)
MessagingModuleInter-service event broadcasting via Redis Streams

Bootstrap

startApp()

Standard app entry point. Handles environment loading, global prefix, URL versioning, Helmet, compression, validation, and Swagger.

// apps/my-service/src/main.ts
import { startApp } from '@tsdevstack/nest-common';
import { AppModule } from './app.module';

startApp(AppModule);

startApp reads service metadata from package.json, loads framework config, and starts the app on 0.0.0.0:{port}. Swagger docs are served at /api in non-production environments.

startWorker()

Worker entry point for background job processors. Creates a NestJS application context without an HTTP server.

// apps/auth-service/src/worker.ts
import { startWorker } from '@tsdevstack/nest-common';
import { WorkerModule } from './worker.module';

startWorker(WorkerModule);

Features:

  • Health endpoint at GET /health on port 8080 (configurable)
  • Graceful shutdown with 9-second timeout
  • SIGTERM/SIGINT signal handlers

Authentication

AuthModule provides gateway-integrated authentication. Kong validates tokens at the gateway level, and AuthGuard (registered globally with APP_GUARD in every generated service) verifies that the request came through Kong, then reads the identity Kong set.

import { Controller, Get, Post, Req } from '@nestjs/common';
import { ApiBearerAuth } from '@nestjs/swagger';
import { Public, PartnerApi, Partner, Roles } from '@tsdevstack/nest-common';
import type { AuthenticatedRequest } from '@tsdevstack/nest-common';

@Controller('offers')
export class OffersController {
  // JWT-protected endpoint
  @Get('mine')
  @ApiBearerAuth()
  async getMyOffers(@Req() req: AuthenticatedRequest) {
    const userId = req.user.id; // from JWT sub claim
  }

  // Admins only (system or custom roles)
  @Get('all')
  @ApiBearerAuth()
  @Roles('ADMIN')
  async getAllOffers() {}

  // Dual access: JWT for users, API key for partners
  @Get('export')
  @ApiBearerAuth()
  @PartnerApi()
  async exportOffers(
    @Req() req: AuthenticatedRequest,
    @Partner() partner?: string,
  ) {
    if (partner) {
      // API key call: partner is the key's consumer name
    } else {
      // JWT call — use req.user
    }
  }

  // Public endpoint (no auth required)
  @Post('login')
  @Public()
  async login() {}
}

Decorators

DecoratorEffect
@Public()Allow anonymous callers on this endpoint
@PartnerApi()Enable API key access under /api/ prefix. Can combine with @ApiBearerAuth() for dual access. Partner keys get 403 on handlers without it
@Partner()Parameter decorator: the consumer name of the partner API key (req.apiKey.consumer)
@ApiKey()Parameter decorator: the partner API key that authenticated the request ({ id, consumer } from the gateway's X-Api-Key-Id and X-Api-Key-Consumer headers, never the raw key)
@Roles(...roles)Require at least one of the given system or custom roles (403 otherwise). Applies RolesGuard. See Roles

RolesGuard and ROLES_KEY (the metadata key @Roles() sets) are exported for custom decorators.

Requests are classified trust first: identity headers only count when the Kong trust token (X-Kong-Trust) is valid. Without it, the caller is an internal service with the service API_KEY, an anonymous caller on a @Public() handler, or rejected with 401. Details in Protected Routes.

Types

import type {
  KongUser,
  AuthenticatedRequest,
  AuthenticatedApiKey,
  AuthType,
} from '@tsdevstack/nest-common';

// KongUser: user identity from the JWT (X-Userinfo)
interface KongUser {
  id: string;   // sub claim; systemRole, roles, email, ... as further keys
  [key: string]: string | string[] | number | boolean | undefined;
}

type AuthType = 'user' | 'apiKey' | 'service';

// AuthenticatedApiKey: the partner key that authenticated the request
interface AuthenticatedApiKey {
  id: string;
  consumer: string;
}

// AuthenticatedRequest — Express request with auth data
interface AuthenticatedRequest extends Request {
  authType?: AuthType;           // undefined for anonymous public calls
  user?: KongUser;               // authType 'user'
  apiKey?: AuthenticatedApiKey;  // authType 'apiKey'
  service?: string;              // calling service, or 'partner' for API keys
  viaGateway?: boolean;          // true when the Kong trust token was valid
}

KongHeaders lists the header names AuthGuard reads. The X-JWT-Claim-* headers are no longer read, and KongHeaders.JWT_CLAIM_PREFIX was removed; read claims from req.user instead.

For deeper coverage, see Authentication Overview.

Secrets

SecretsModule provides unified secret access across all cloud providers. It auto-detects the provider from the SECRETS_PROVIDER environment variable.

import { Injectable } from '@nestjs/common';
import { SecretsService } from '@tsdevstack/nest-common';

@Injectable()
export class PaymentService {
  constructor(private secrets: SecretsService) {}

  async charge(amount: number): Promise<void> {
    const stripeKey = await this.secrets.get('STRIPE_SECRET_KEY');
    // ...
  }
}
Info

Always use SecretsService — never process.env. The secrets system handles provider detection, caching, and service-scoped access automatically.

Providers

ProviderWhenCache TTL
localLocal development1 minute
gcpGoogle Cloud Secret Manager5 minutes
awsAWS Secrets Manager5 minutes
azureAzure Key Vault5 minutes

Methods

await secrets.get('KEY');        // Get secret value (cached)
await secrets.set('KEY', 'val'); // Set secret
await secrets.delete('KEY');     // Delete secret
secrets.clearCache();            // Clear cache

For deeper coverage, see How Secrets Work.

Redis

RedisModule provides a global Redis client with automatic configuration from secrets.

import { Injectable } from '@nestjs/common';
import { RedisService } from '@tsdevstack/nest-common';

@Injectable()
export class CacheService {
  constructor(private redis: RedisService) {}

  async cacheResult(key: string, value: string): Promise<void> {
    await this.redis.set(key, value, 300); // 300s TTL
  }

  async getCached(key: string): Promise<string | null> {
    return this.redis.get(key);
  }
}

Methods

MethodReturnDescription
get(key)Promise<string | null>Get value
set(key, value, ttl?)Promise<boolean>Set value with optional TTL in seconds
del(key)Promise<boolean>Delete key
incr(key)Promise<number | null>Increment counter
expire(key, seconds)Promise<boolean>Set key expiration
getClient()RedisGet underlying ioredis client
isReady()booleanWhether the connection can run commands right now
onReady(listener)() => voidCall listener every time the connection becomes ready (after startup and after each reconnect); returns an unsubscribe function

onReady is useful for rebuilding data that a Redis restart wiped. A listener added after the first connect is not called for it, so check isReady() when you subscribe if the current state matters.

Configuration

Reads from secrets automatically:

SecretDefaultDescription
REDIS_HOST—Redis host
REDIS_PORT6379Redis port
REDIS_PASSWORD—Redis password
REDIS_TLS—Enable TLS (for AWS ElastiCache)

Connection: 10s connect timeout, 30s keep-alive. After a lost connection the client reconnects forever, with exponential backoff capped at 5 seconds, so a Redis restart or failover of any length is survived without restarting the service. While disconnected, commands fail immediately instead of queueing; get returns null and the rate limit guards fail open.

API key index

The gateway checks partner API keys against records in Redis. The auth-service template writes them; projects that manage keys with their own tooling can use the same contract from code instead of re-implementing it:

import {
  hashApiKey,
  buildApiKeyRecordKey,
  encodeApiKeyRecord,
  getApiKeyRecordExpireAt,
  API_KEY_INDEX_MARKER_KEY,
} from '@tsdevstack/nest-common';
import type { ApiKeyRecord } from '@tsdevstack/nest-common';
ExportPurpose
hashApiKey(rawKey)SHA-256 hex of the key, as the gateway computes it
buildApiKeyRecordKey(hash), buildApiKeyCounterKey(hash, window, now), buildApiKeyLastUsedKey(hash)Redis key names (apikey:{<hash>}:...)
encodeApiKeyRecord(record), decodeApiKeyRecord(json), validateApiKeyRecord(value)Serialize, parse and check a record against the contract
getApiKeyRecordExpireAt(record, now)The Redis expiry of a record (epoch seconds, or null for none)
getApiKeyWindowStart, getApiKeyWindowEnd, getApiKeyWindowId, getApiKeyCounterExpireAtUTC limit windows (minute, hour, day, ISO week, month)
ApiKeyRecord, ApiKeyRecordLimits, ApiKeyRecordStatus, ApiKeyRecordValidation, ApiKeyWindowTypes
API_KEY_* constantsRedis prefix, marker and lock names, record versions, windows, statuses, TTLs, identifier pattern, limit maximum

The record format, key names and TTL rules are a public, versioned contract, documented in API Keys.

Background jobs

BullConfigModule

Configures BullMQ with Redis from SecretsService. Portable across all providers.

import { Module } from '@nestjs/common';
import { BullConfigModule } from '@tsdevstack/nest-common';
import { BullModule } from '@nestjs/bullmq';
import { EmailProcessor } from './processors/email.processor';

@Module({
  imports: [
    BullConfigModule.forRoot(),
    BullModule.registerQueue({ name: 'emails' }),
  ],
  providers: [EmailProcessor],
})
export class WorkerModule {}

Default job options: 3 retries with exponential backoff, keep 100 completed / 500 failed jobs.

SchedulerGuard

Validates that requests to scheduled job endpoints come from the cloud scheduler (not arbitrary HTTP clients).

import { Controller, Post, UseGuards } from '@nestjs/common';
import { SchedulerGuard } from '@tsdevstack/nest-common';

@Controller('jobs')
export class JobsController {
  @Post('cleanup-tokens')
  @UseGuards(SchedulerGuard)
  async cleanupTokens() {
    // Only runs when called by cloud scheduler
  }
}

Validation is provider-aware:

ProviderValidation method
LocalSkipped (all requests allowed)
GCPOIDC token verification
AWSX-Job-Secret header (EventBridge shared secret)
AzureX-Job-Secret header (Container App Jobs)

Service-to-service communication

BaseServiceClient

Abstract base class for typed HTTP clients. Use with generated clients from generate-client.

import { Injectable, OnModuleInit } from '@nestjs/common';
import { BaseServiceClient, SecretsService } from '@tsdevstack/nest-common';
import { Api } from '@shared/auth-service-client';

@Injectable()
export class AuthClient extends BaseServiceClient<Api<unknown>> implements OnModuleInit {
  constructor(private secrets: SecretsService) {
    super();
  }

  async onModuleInit(): Promise<void> {
    const baseURL = await this.secrets.get('AUTH_SERVICE_URL');
    const apiKey = await this.secrets.get('AUTH_SERVICE_API_KEY');

    this.initialize({
      baseURL,
      apiKey,
      createClient: (url, key) =>
        new Api({ baseURL: url, headers: { 'x-api-key': key } }),
    });
  }
}

// Usage in another service:
// this.authClient.client.v1.getUserProfile()

filterForwardHeaders()

Filters request headers for safe forwarding in service-to-service calls. Removes hop-by-hop headers (connection, keep-alive, transfer-encoding, host, content-length, content-encoding).

import { filterForwardHeaders } from '@tsdevstack/nest-common';

const safeHeaders = filterForwardHeaders(req.headers);
await this.authClient.client.v1.someEndpoint({ headers: safeHeaders });

Rate limiting

General rate limiting

Redis-backed sliding window rate limiting with configurable key generators.

import { Controller, Get, UseGuards } from '@nestjs/common';
import { RateLimitGuard, RateLimit } from '@tsdevstack/nest-common';

@Controller('api')
export class ApiController {
  @Get('search')
  @UseGuards(RateLimitGuard)
  @RateLimit({
    windowMs: 60_000,       // 1 minute window
    maxRequests: 30,         // 30 requests per window
    keyGenerator: 'ip',      // Rate limit by IP address
  })
  async search() {}
}

Options

OptionTypeDefaultDescription
windowMsnumber900000 (15 min)Window size in milliseconds
maxRequestsnumber100Max requests per window
keyGenerator'ip' | 'apiKey' | 'userId' | 'custom''ip'How to identify clients (see below)
customKeyGenerator(context) => string—Custom key function
skipIf(context) => boolean—Skip rate limiting conditionally
messagestring—Custom error message

Key generators:

  • ip: the client IP. That is X-Real-IP (set by Kong) only for requests that passed the Kong trust check; for anything else, such as direct calls to the service, the socket address. X-Forwarded-For is never read, because clients control its first entry.
  • userId: the logged-in user's id. Partner API key requests on dual-access endpoints are limited per key id instead. Anything else gets 401.
  • apiKey: partner key requests per key id, internal service calls per (hashed) service key, everyone else per IP.

Fail-open: if Redis is unavailable, requests are allowed through.

Email rate limiting

Per-email address rate limiting for auth flows (signup, password reset, verification).

import { Controller, Post, UseGuards, Body } from '@nestjs/common';
import { EmailRateLimitGuard, EmailRateLimit } from '@tsdevstack/nest-common';

@Controller('auth')
export class AuthController {
  @Post('forgot-password')
  @UseGuards(EmailRateLimitGuard)
  @EmailRateLimit({
    windowMs: 900_000,     // 15 minutes
    maxRequests: 3,         // 3 attempts per email
    emailField: 'email',   // field name in request body
  })
  async forgotPassword(@Body() body: { email: string }) {}
}

Notifications

NotificationModule provides email sending with automatic provider selection.

import { Injectable } from '@nestjs/common';
import { NotificationService } from '@tsdevstack/nest-common';

@Injectable()
export class AuthService {
  constructor(private notifications: NotificationService) {}

  async sendVerification(email: string, link: string): Promise<void> {
    await this.notifications.sendEmail({
      to: email,
      subject: 'Verify your email',
      html: `<a href="${link}">Verify your email address</a>`,
    });
  }
}

Email providers

ProviderWhenBehavior
consoleLocal development (default)Logs to terminal with sender, recipient, subject, body
resendProductionSends via Resend API

Provider is selected by the EMAIL_PROVIDER secret. For production setup (account creation, domain verification, DNS records), see Resend setup.

Custom email provider

You can replace Resend with SendGrid, Mailgun, Postmark, or any other provider by implementing the EmailProvider interface and overriding the EMAIL_PROVIDER token. See Custom Email Provider for a full walkthrough.

Storage

StorageModule provides unified object storage across all cloud providers. The same code works on local MinIO, AWS S3, GCP Cloud Storage, and Azure Blob Storage.

import { Module } from '@nestjs/common';
import { StorageModule } from '@tsdevstack/nest-common';

@Module({
  imports: [
    StorageModule.forRoot({ buckets: ['uploads'] }),
  ],
})
export class AppModule {}

Use @InjectStorage to get a StorageProvider for a specific bucket:

import { Injectable } from '@nestjs/common';
import { InjectStorage } from '@tsdevstack/nest-common';
import type { StorageProvider } from '@tsdevstack/nest-common';

@Injectable()
export class FileService {
  constructor(
    @InjectStorage('uploads') private readonly storage: StorageProvider,
  ) {}

  async uploadFile(key: string, data: Buffer, contentType: string): Promise<void> {
    await this.storage.upload(key, data, contentType);
  }

  async getDownloadUrl(key: string): Promise<string> {
    return this.storage.getPresignedUrl(key, 3600);
  }
}

Methods

MethodReturnDescription
upload(key, data, contentType?)Promise<void>Upload file (Buffer or stream)
download(key)Promise<Buffer>Download file as Buffer
downloadStream(key)Promise<Readable>Download as readable stream
delete(key)Promise<void>Delete file (no error if missing)
list(prefix?)Promise<StorageObject[]>List files by prefix
copy(src, dest)Promise<void>Copy within same bucket
getMetadata(key)Promise<StorageMetadata>Get file metadata
getPresignedUrl(key, expiresIn?)Promise<string>Generate temporary download URL
exists(key)Promise<boolean>Check if file exists
getNativeClient()Provider SDK clientGet underlying S3/GCS/Blob client

Provider selection

The adapter is selected automatically from the SECRETS_PROVIDER environment variable:

SECRETS_PROVIDERAdapterEnvironment
localS3 (MinIO)Local development
awsS3 (AWS)AWS cloud
gcpGCSGoogle Cloud
azureAzure BlobAzure cloud
Info

Never install @aws-sdk/client-s3, @google-cloud/storage, or @azure/storage-blob directly — StorageModule handles all provider SDKs internally.

For setup instructions, bucket naming, secrets, and cloud deployment details, see Object Storage.

Messaging

MessagingModule provides inter-service event broadcasting via Redis Streams. Services publish events to topics, and all subscribing services receive every message independently.

import { Module } from '@nestjs/common';
import { MessagingModule } from '@tsdevstack/nest-common';
import { UserEventsHandler } from './handlers/user-events.handler';

@Module({
  imports: [
    MessagingModule.forRoot({
      consumerGroup: 'offers-service',
      topics: ['user-created'],
    }),
  ],
  providers: [UserEventsHandler],
})
export class AppModule {}

MessagingModule.forRoot() options

OptionTypeDefaultDescription
consumerGroupstring—This service's name (e.g., 'offers-service')
topicsstring[][]Topics to subscribe to
maxRetriesnumber3Delivery attempts before sending to DLQ
blockTimeMsnumber5000XREADGROUP block time in ms
maxLennumber10000Stream MAXLEN trim per topic
claimMinIdleMsnumber60000Reclaim stuck messages after (ms)

forRootAsync is available as an escape hatch for dynamic configuration.

Publishing-only services (no subscriptions) omit topics:

MessagingModule.forRoot({ consumerGroup: 'auth-service' })

Publishing

import { Injectable } from '@nestjs/common';
import { MessagingService } from '@tsdevstack/nest-common';

@Injectable()
export class AuthService {
  constructor(private messaging: MessagingService) {}

  async register(dto: RegisterDto): Promise<User> {
    const user = await this.usersRepo.save(dto);
    await this.messaging.publish('user-created', {
      userId: user.id,
      email: user.email,
    });
    return user;
  }
}

publish() returns the Redis stream entry ID.

Subscribing — @OnMessage decorator

import { Injectable } from '@nestjs/common';
import { OnMessage } from '@tsdevstack/nest-common';
import type { IncomingMessage } from '@tsdevstack/nest-common';

@Injectable()
export class UserEventsHandler {
  @OnMessage('user-created')
  async handleUserCreated(message: IncomingMessage): Promise<void> {
    const { userId } = message.data as { userId: string };
    await this.offersRepo.createDefaultProfile(userId);
    // Returning without error → auto-XACK (acknowledged)
  }
}

Handler contract:

  • Handler returns (resolves) → message is XACK'd
  • Handler throws → message stays pending, will be retried
  • After maxRetries attempts → message moves to DLQ stream

IncomingMessage

interface IncomingMessage {
  id: string;            // Redis stream entry ID (e.g., '1709312000000-0')
  topic: string;         // Stream name
  data: Record<string, unknown>; // Parsed message payload
  publishedAt: Date;     // Extracted from stream ID timestamp
  retryCount: number;    // Delivery attempts so far
}

Error handling

import { MessagingError, MessagingErrorCode } from '@tsdevstack/nest-common';

// MessagingErrorCode values:
// PUBLISH_FAILED — XADD failed
// CONSUMER_FAILED — consumer loop error
// SERIALIZATION_FAILED — JSON parse error

Graceful shutdown

On SIGTERM/SIGINT, the module stops accepting new messages, waits for in-flight handlers to complete (5s timeout), and disconnects. Pending messages are reclaimed by another instance or retried on next startup.

For topic management, naming conventions, and flow diagrams, see Async Messaging.

Observability

ObservabilityModule bundles structured logging, Prometheus metrics, distributed tracing, and health checks into a single import.

import { Module } from '@nestjs/common';
import { ObservabilityModule } from '@tsdevstack/nest-common';

@Module({
  imports: [ObservabilityModule],
})
export class AppModule {}

This enables:

  • Logging — Structured JSON via Pino with PII redaction and trace context
  • Metrics — Prometheus at /metrics with automatic HTTP request tracking
  • Tracing — Distributed traces to Jaeger via OpenTelemetry
  • Health — /health endpoint with Redis and memory checks

For configuration, custom metrics, manual spans, and cloud setup, see Observability.

Database

createPrismaConnection()

Creates a Prisma client with connection pooling optimized for containers.

import { createPrismaConnection } from '@tsdevstack/nest-common';
import { PrismaClient } from '@prisma/client';

const { config, pool } = createPrismaConnection();
const prisma = new PrismaClient(config);

// On shutdown:
await prisma.$disconnect();
pool.end();

Uses Prisma 7 with the pg adapter.

Pool sizing

In local development, the pool defaults to 5 connections.

In cloud environments, the framework automatically calculates DB_POOL_MAX at deploy time based on:

  1. The database tier's connection limit (e.g., db-f1-micro = 25 connections on GCP)
  2. 10% reserved for admin/migrations
  3. Remaining connections split evenly across all service + worker instances (using maxInstances from infrastructure.json)
totalUsable = floor(dbConnections × 0.90)
poolMax     = floor(totalUsable / totalInstances)

You never set DB_POOL_MAX manually — it's injected as an environment variable during deployment.

Connection options

  • SSL for AWS RDS (rejectUnauthorized: false)
  • 30-second idle timeout
  • 10-second connection timeout

Health checks

The health system is included in ObservabilityModule. It exposes two endpoints:

EndpointPurpose
/healthFull health check with component status
/health/pingSimple liveness probe

Response format

{
  "status": "ok",
  "timestamp": "2026-01-15T10:30:45.000Z",
  "uptime": 3600.5,
  "checks": {
    "redis": { "status": "up" },
    "memory": { "status": "up", "details": { "heapUsed": 45, "heapTotal": 128 } }
  },
  "memory": {
    "used": 45,
    "total": 128
  }
}

Status values: ok, degraded, down. When Redis is disconnected, the Redis check reports down and the overall status is degraded; the endpoint still answers HTTP 200, so the service is not restarted over a Redis outage, but monitoring that reads the body will see it.

Health indicators

IndicatorWhat it checks
RedisIs the connection ready and does Redis answer PING within 2 seconds?
MemoryIs heap usage below the threshold? (default: 90%)

Health and metrics endpoints are @Public() and excluded from API docs. They bypass Kong's trust header so load balancers and monitoring systems can access them directly.