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
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
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
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
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:
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';
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:
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()
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
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
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
Provider selection
The adapter is selected automatically from the SECRETS_PROVIDER environment variable:
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
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:
- The database tier's connection limit (e.g.,
db-f1-micro = 25 connections on GCP)
- 10% reserved for admin/migrations
- 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:
{
"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
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.