@digitaldefiance/node-express-suite
v4.27.1
Published
Generic express application and routing library with decorator support
Maintainers
Readme
@digitaldefiance/node-express-suite
An opinionated, secure, extensible Node.js/Express service framework built on Digital Defiance cryptography libraries, providing complete backend infrastructure for secure applications.
Node Express Suite forms the base of an 'out of the box' solution for websites using a specific recipe with JWT authentication, role-based access control, custom multi-language support via @digitaldefiance/i18n-lib, and a dynamic model registry system. You might either find it limiting or freeing, depending on your use case. It includes mnemonic authentication, ECIES encryption/decryption, PBKDF2 key derivation, email token workflows, and more.
Originally Node Express Suite was entirely Mongo based, but BrightChain evolved and developed its own @brightchain/node-express-suite that mirrored this package and the mongo components were moved to @digitaldefiance/node-express-suite-mongo. Now this package is database-agnostic and forms the base for both the Mongo package and BrightStack's version of node-express-suite.
Note: Starting with v5.0, all MongoDB/Mongoose-specific code (documents, schemas, models,
MongoDatabasePlugin,DatabaseInitializationService,UserService,RoleService,ModelRegistry, etc.) has been extracted into@digitaldefiance/node-express-suite-mongo. If your application uses MongoDB, install both packages. See MONGO_SPLIT_MIGRATION.md for details.
Part of Express Suite
What's New in v3.12.0
✨ Comprehensive Decorator API for Express Controllers - A complete decorator-based API for defining controllers, routes, validation, and OpenAPI documentation with automatic spec generation.
New Decorators:
| Category | Decorators |
|----------|------------|
| Controller | @Controller, @ApiController |
| HTTP Methods | @Get, @Post, @Put, @Delete, @Patch (enhanced with OpenAPI support) |
| Authentication | @RequireAuth, @RequireCryptoAuth, @Public, @AuthFailureStatus |
| Parameters | @Param, @Body, @Query, @Header, @CurrentUser, @EciesUser, @Req, @Res, @Next |
| Validation | @ValidateBody, @ValidateParams, @ValidateQuery (Zod & express-validator) |
| Response | @Returns, @ResponseDoc, @RawJson, @Paginated |
| Middleware | @UseMiddleware, @CacheResponse, @RateLimit |
| Transaction | @Transactional |
| OpenAPI | @ApiOperation, @ApiTags, @ApiSummary, @ApiDescription, @Deprecated, @ApiOperationId, @ApiExample |
| OpenAPI Params | @ApiParam, @ApiQuery, @ApiHeader, @ApiRequestBody |
| Lifecycle | @OnSuccess, @OnError, @Before, @After |
| Schema | @ApiSchema, @ApiProperty |
Key Features:
- Full RouteConfig Parity: Every feature available in manual
RouteConfighas a decorator equivalent - Automatic OpenAPI Generation: Decorators automatically generate complete OpenAPI 3.0.3 specifications
- Zod Integration: Zod schemas are automatically converted to OpenAPI request body schemas
- Metadata Merging: Multiple decorators on the same method merge their OpenAPI metadata
- Class-Level Inheritance: Class-level decorators (auth, tags, middleware) apply to all methods unless overridden
- Parameter Injection: Clean, typed parameter injection with
@Param,@Body,@Query,@Header - Lifecycle Hooks:
@Before,@After,@OnSuccess,@OnErrorfor request lifecycle management
Documentation Middleware:
SwaggerUIMiddleware- Serve Swagger UI with customization optionsReDocMiddleware- Serve ReDoc documentationgenerateMarkdownDocs()- Generate markdown documentation from OpenAPI spec
Migration Support:
- Full backward compatibility with existing
RouteConfigapproach - Comprehensive migration guide in
docs/DECORATOR_MIGRATION.md - Mixed usage supported (decorated + manual routes in same controller)
What's New in v3.11.25
✨ String Key Enum Registration & i18n v4 Integration - Upgraded to @digitaldefiance/i18n-lib v4.0.5 with branded enum translation support and translateStringKey() for automatic component ID resolution.
New Features:
- Direct String Key Translation: Use
translateStringKey()without specifying component IDs - Automatic Component Routing: Branded enums resolve to their registered components automatically
- Safe Translation:
safeTranslateStringKey()returns placeholder on failure instead of throwing
Dependencies:
@digitaldefiance/ecies-lib: 4.15.1 → 4.16.0@digitaldefiance/i18n-lib: 4.0.3 → 4.0.5@digitaldefiance/node-ecies-lib: 4.15.1 → 4.16.0@digitaldefiance/suite-core-lib: 3.9.1 → 3.10.0@noble/curves: 1.4.2 → 1.9.0@noble/hashes: 1.4.0 → 1.8.0
What's New in v3.8.0
✨ Dependency Upgrades & API Alignment - Upgraded to @digitaldefiance/ecies-lib v4.13.0, @digitaldefiance/node-ecies-lib v4.13.0, and @digitaldefiance/suite-core-lib v3.7.0.
Breaking Changes:
- Encryption API renamed:
encryptSimpleOrSingle()→encryptBasic()/encryptWithLength() - Decryption API renamed:
decryptSimpleOrSingleWithHeader()→decryptBasicWithHeader()/decryptWithLengthAndHeader() - Configuration change:
registerNodeRuntimeConfiguration()now requires a key parameter - XorService behavior change: Now throws error when key is shorter than data (previously cycled the key)
- Removed constants:
OBJECT_ID_LENGTHremoved fromIConstants- useidProvider.byteLengthinstead
Interface Changes:
IConstantsnow extends bothINodeEciesConstantsandISuiteCoreConstants- Added
ECIES_CONFIGto configuration - Encryption mode constants renamed:
SIMPLE→BASIC,SINGLE→WITH_LENGTH
Features
- 🔐 ECIES Encryption/Decryption: End-to-end encryption using elliptic curve cryptography
- 🔑 PBKDF2 Key Derivation: Secure password hashing with configurable profiles
- 👥 Role-Based Access Control (RBAC): Flexible permission system with user roles
- 🌍 Multi-Language i18n: Plugin-based internationalization with 8+ languages
- 🔌 Plugin-Based Database: Database-agnostic core with pluggable backends (see
node-express-suite-mongofor MongoDB) - 🔧 Runtime Configuration: Override defaults at runtime for advanced use cases
- 🛡️ JWT Authentication: Secure token-based authentication
- 📧 Email Token System: Verification, password reset, and recovery workflows
- 🧪 Comprehensive Testing: 100+ tests covering all major functionality
- 🏗️ Modern Architecture: Service container, fluent builders, plugin system
- ⚡ Simplified Generics: 87.5% reduction in type complexity
- 🔄 Automatic Transactions: Decorator-based transaction management
- 🎨 Fluent APIs: Validation, response, pipeline, and route builders
- 🎯 Decorator API: Full decorator-based controller system with automatic OpenAPI generation
Installation
npm install @digitaldefiance/node-express-suite
# or
yarn add @digitaldefiance/node-express-suiteQuick Start
Basic Server Setup (Database-Agnostic)
import { Application, ServiceKeys } from '@digitaldefiance/node-express-suite';
import { LanguageCodes } from '@digitaldefiance/i18n-lib';
import { EmailService } from './services/email'; // Your concrete implementation
// Create application instance
const app = new Application(environment, apiRouterFactory);
// Register email service (required before using middleware)
app.services.register(ServiceKeys.EMAIL, () => new EmailService(app));
// Start server
await app.start();
console.log(`Server running on port ${app.environment.port}`);With MongoDB (using the Mongo Package)
import { Application } from '@digitaldefiance/node-express-suite';
import {
MongoDatabasePlugin,
DatabaseInitializationService,
getSchemaMap,
} from '@digitaldefiance/node-express-suite-mongo';
const app = new Application(environment, apiRouterFactory);
const mongoPlugin = new MongoDatabasePlugin({
schemaMapFactory: getSchemaMap,
databaseInitFunction: DatabaseInitializationService.initUserDb.bind(DatabaseInitializationService),
initResultHashFunction: DatabaseInitializationService.serverInitResultHash.bind(DatabaseInitializationService),
environment,
constants,
});
app.useDatabasePlugin(mongoPlugin);
await app.start();User Authentication
import { JwtService } from '@digitaldefiance/node-express-suite';
// UserService is in the mongo package if using MongoDB
import { UserService } from '@digitaldefiance/node-express-suite-mongo';
const jwtService = new JwtService(app);
const userService = new UserService(app);
const user = await userService.findByUsername('alice');
const { token, roles } = await jwtService.signToken(user, app.environment.jwtSecret);
const tokenUser = await jwtService.verifyToken(token);
console.log(`User ${tokenUser.userId} authenticated with roles:`, tokenUser.roles);Core Components
Plugin-Based Database Architecture
The framework uses a plugin-based architecture that separates database concerns from the core application. Any database backend can be used by implementing the IDatabasePlugin interface.
import { Application } from '@digitaldefiance/node-express-suite';
// Database-agnostic — plug in any backend
const app = new Application(environment, apiRouterFactory);
app.useDatabasePlugin(myDatabasePlugin);
await app.start();For MongoDB/Mongoose support, install @digitaldefiance/node-express-suite-mongo which provides MongoDatabasePlugin, ModelRegistry, documents, schemas, models, and all Mongoose-specific services. See that package's README for details on model registration, schema extension, and custom enumerations.
Services
Note:
UserService,RoleService,BackupCodeService, andDatabaseInitializationServicehave moved to@digitaldefiance/node-express-suite-mongo. The services below remain in this package.
ECIESService
Encryption and key management:
import { ECIESService } from '@digitaldefiance/node-express-suite';
const eciesService = new ECIESService();
// Generate mnemonic
const mnemonic = eciesService.generateNewMnemonic();
// Encrypt data
const encrypted = await eciesService.encryptWithLength(
recipientPublicKey,
Buffer.from('secret message')
);
// Decrypt data
const decrypted = await eciesService.decryptWithLengthAndHeader(
privateKey,
encrypted
);KeyWrappingService
Secure key storage and retrieval:
import { KeyWrappingService } from '@digitaldefiance/node-express-suite';
const keyWrapping = new KeyWrappingService(app);
// Wrap a key with password
const wrapped = await keyWrapping.wrapKey(
privateKey,
password,
salt
);
// Unwrap key
const unwrapped = await keyWrapping.unwrapKey(
wrapped,
password,
salt
);RoleService
Available in
@digitaldefiance/node-express-suite-mongo.
import { RoleService } from '@digitaldefiance/node-express-suite-mongo';
const roleService = new RoleService(app);
// Get user roles
const roles = await roleService.getUserRoles(userId);
// Check permissions
const hasPermission = await roleService.userHasRole(userId, 'admin');
// Create role
const adminRole = await roleService.createRole({
name: 'admin',
description: 'Administrator role',
permissions: ['read', 'write', 'delete']
});BackupCodeService
Available in
@digitaldefiance/node-express-suite-mongo.
import { BackupCodeService } from '@digitaldefiance/node-express-suite-mongo';
const backupCodeService = new BackupCodeService(app);
// Generate backup codes
const codes = await backupCodeService.generateBackupCodes(userId);
// Validate code
const isValid = await backupCodeService.validateBackupCode(userId, userCode);
// Mark code as used
await backupCodeService.useBackupCode(userId, userCode);Database Initialization
Available in
@digitaldefiance/node-express-suite-mongo.
import { DatabaseInitializationService } from '@digitaldefiance/node-express-suite-mongo';
// Initialize with default admin, member, and system users
const result = await DatabaseInitializationService.initUserDb(app);
if (result.success) {
console.log('Admin user:', result.data.adminUsername);
console.log('Admin password:', result.data.adminPassword);
console.log('Admin mnemonic:', result.data.adminMnemonic);
console.log('Backup codes:', result.data.adminBackupCodes);
}Middleware
Email Service Configuration
Before using middleware that requires email functionality, configure the email service:
import { ServiceKeys, IEmailService } from '@digitaldefiance/node-express-suite';
// Implement the IEmailService interface
class MyEmailService implements IEmailService {
async sendEmail(to: string, subject: string, text: string, html: string): Promise<void> {
// Your email implementation (AWS SES, SendGrid, etc.)
}
}
// Register at application startup via the ServiceContainer
app.services.register(ServiceKeys.EMAIL, () => new MyEmailService());Authentication Middleware
import { authMiddleware } from '@digitaldefiance/node-express-suite';
// Protect routes with JWT authentication
app.get('/api/protected', authMiddleware, (req, res) => {
// req.user contains authenticated user info
res.json({ user: req.user });
});Role-Based Authorization
import { requireRole } from '@digitaldefiance/node-express-suite';
// Require specific role
app.delete('/api/users/:id',
authMiddleware,
requireRole('admin'),
async (req, res) => {
// Only admins can access this route
await userService.deleteUser(req.params.id);
res.json({ success: true });
}
);Runtime Configuration Registry
Override defaults at runtime for advanced use cases:
import {
getExpressRuntimeConfiguration,
registerExpressRuntimeConfiguration,
} from '@digitaldefiance/node-express-suite';
// Get current configuration
const config = getExpressRuntimeConfiguration();
console.log('Bcrypt rounds:', config.BcryptRounds);
// Register custom configuration
const customKey = Symbol('custom-express-config');
registerExpressRuntimeConfiguration(customKey, {
BcryptRounds: 12,
JWT: {
ALGORITHM: 'HS512',
EXPIRATION_SEC: 7200
}
});
// Use custom configuration
const customConfig = getExpressRuntimeConfiguration(customKey);Available Configuration Options
interface IExpressRuntimeConfiguration {
BcryptRounds: number;
JWT: {
ALGORITHM: string;
EXPIRATION_SEC: number;
};
BACKUP_CODES: {
Count: number;
Length: number;
};
// ... more options
}Internationalization
Built-in support for multiple languages using the plugin-based i18n architecture:
import { getGlobalI18nEngine, translateExpressSuite } from '@digitaldefiance/node-express-suite';
import { LanguageCodes } from '@digitaldefiance/i18n-lib';
// Get the global i18n engine
const i18n = getGlobalI18nEngine();
// Translate strings using branded string keys (v3.11.0+)
// Component ID is automatically resolved from the branded enum
const message = translateExpressSuite(
ExpressSuiteStringKey.Common_Ready,
{},
LanguageCodes.FR
);
// "Prêt"
// Change language globally
i18n.setLanguage(LanguageCodes.ES);Direct String Key Translation
Starting with v3.11.0, the library uses translateStringKey() internally for automatic component ID resolution from branded enums. This means you can also use the engine directly:
import { getGlobalI18nEngine } from '@digitaldefiance/node-express-suite';
import { ExpressSuiteStringKey } from '@digitaldefiance/node-express-suite';
const engine = getGlobalI18nEngine();
// Direct translation - component ID resolved automatically
const text = engine.translateStringKey(ExpressSuiteStringKey.Common_Ready);
// Safe version returns placeholder on failure
const safe = engine.safeTranslateStringKey(ExpressSuiteStringKey.Common_Ready);Supported Languages
- English (US)
- Spanish
- French
- Mandarin Chinese
- Japanese
- German
- Ukrainian
Error Handling
Comprehensive error types with localization:
import {
TranslatableError,
InvalidJwtTokenError,
TokenExpiredError,
UserNotFoundError
} from '@digitaldefiance/node-express-suite';
try {
const user = await userService.findByEmail(email);
} catch (error) {
if (error instanceof UserNotFoundError) {
// Handle user not found
res.status(404).json({
error: error.message // Automatically localized
});
} else if (error instanceof TranslatableError) {
// Handle other translatable errors
res.status(400).json({ error: error.message });
}
}Testing
Comprehensive test suite with 2541 passing tests:
# Run all tests
npm test
# Run specific test suites
npm test -- database-initialization.spec.ts
npm test -- jwt.spec.ts
npm test -- role.spec.ts
# Run with coverage
npm test -- --coverageTest Utilities
Test helpers and mocks are available via the /testing entry point:
// Import test utilities
import {
mockFunctions,
setupTestEnv,
// ... other test helpers
} from '@digitaldefiance/node-express-suite/testing';
// Use in your tests
beforeAll(async () => {
await setupTestEnv();
});Note: The /testing entry point requires @faker-js/faker as a peer dependency. Install it in your dev dependencies:
npm install -D @faker-js/faker
# or
yarn add -D @faker-js/fakerTest Coverage (v2.1)
- 2541 tests passing (100% success rate)
- 57.86% overall coverage
- 11 modules at 100% coverage
- All critical paths tested (validation, auth, services)
Let's Encrypt (Automated TLS)
The package supports automated TLS certificate management via Let's Encrypt using greenlock-express. When enabled, the Application automatically obtains and renews certificates, serves your app over HTTPS on port 443, and redirects HTTP traffic from port 80 to HTTPS.
Environment Variables
| Variable | Type | Default | Description |
|---|---|---|---|
| LETS_ENCRYPT_ENABLED | boolean | false | Set to true or 1 to enable Let's Encrypt certificate management |
| LETS_ENCRYPT_EMAIL | string | (required when enabled) | Contact email for your Let's Encrypt account (used for expiry notices and account recovery) |
| LETS_ENCRYPT_HOSTNAMES | string | (required when enabled) | Comma-separated list of hostnames to obtain certificates for (supports wildcards, e.g. *.example.com) |
| LETS_ENCRYPT_STAGING | boolean | false | Set to true or 1 to use the Let's Encrypt staging directory (recommended for testing) |
| LETS_ENCRYPT_CONFIG_DIR | string | ./greenlock.d | Directory for Greenlock configuration and certificate storage |
Example: Single Hostname
LETS_ENCRYPT_ENABLED=true
[email protected]
LETS_ENCRYPT_HOSTNAMES=example.com
LETS_ENCRYPT_STAGING=falseExample: Multiple Hostnames with Wildcard
LETS_ENCRYPT_ENABLED=true
[email protected]
LETS_ENCRYPT_HOSTNAMES=example.com,*.example.com,api.example.com
LETS_ENCRYPT_STAGING=falseWildcard hostnames (e.g. *.example.com) require DNS-01 challenge validation. Ensure your DNS provider supports programmatic record creation or configure the appropriate Greenlock plugin.
Port Requirements
When Let's Encrypt is enabled, the Application binds to three ports:
- Port 443 — HTTPS server with the auto-managed TLS certificate
- Port 80 — HTTP redirect server (301 redirects all traffic to HTTPS, also serves ACME HTTP-01 challenges)
environment.port(default 3000) — Primary HTTP server for internal or health-check traffic
Ports 80 and 443 are privileged ports on most systems. You may need elevated permissions to bind to them:
- Linux: Use
setcapto grant the Node.js binary the capability without running as root:sudo setcap 'cap_net_bind_service=+ep' $(which node) - Docker: Map the ports in your container configuration (containers typically run as root internally)
- Reverse proxy: Alternatively, run behind a reverse proxy (e.g. nginx) that handles ports 80/443 and forwards to your app
Mutual Exclusivity with Dev-Certificate HTTPS
Let's Encrypt mode and the dev-certificate HTTPS mode (HTTPS_DEV_CERT_ROOT) are mutually exclusive at runtime. When LETS_ENCRYPT_ENABLED=true, the dev-certificate HTTPS block is automatically skipped to avoid port conflicts. You do not need to unset HTTPS_DEV_CERT_ROOT — the Application handles this internally.
- Production: Use
LETS_ENCRYPT_ENABLED=truefor real certificates - Development: Use
HTTPS_DEV_CERT_ROOTfor self-signed dev certificates - Never enable both simultaneously — Let's Encrypt takes precedence when enabled
Best Practices
Security
Always use environment variables for sensitive configuration (JWT secrets, database URIs, API keys)
Validate all user input before processing:
import { EmailString } from '@digitaldefiance/ecies-lib'; try { const email = new EmailString(userInput); // Email is validated } catch (error) { // Invalid email format }Use secure password hashing with appropriate bcrypt rounds:
const config = getExpressRuntimeConfiguration(); const hashedPassword = await bcrypt.hash(password, config.BcryptRounds);
Performance
Use async operations to avoid blocking:
const [user, roles] = await Promise.all([ userService.findById(userId), roleService.getUserRoles(userId) ]);Implement caching for frequently accessed data:
const cachedRoles = await cache.get(`user:${userId}:roles`); if (!cachedRoles) { const roles = await roleService.getUserRoles(userId); await cache.set(`user:${userId}:roles`, roles, 3600); }Use database indexes for common queries (see your database plugin's documentation for index configuration)
API Reference
Application
new Application(environment, apiRouterFactory, ...)— Create application instanceuseDatabasePlugin(plugin)— Register a database pluginstart()— Start the Express serverstop()— Stop the server gracefullyenvironment— Access configuration
Services (this package)
ECIESService— Encryption and key managementKeyWrappingService— Secure key storageJwtService— JWT token operationsChecksumService— CRC checksum operationsSymmetricService— Symmetric encryption operationsXorService— XOR cipher operationsFecService— Forward error correctionDummyEmailService— Test email service implementation
Services (mongo package)
These services are available in @digitaldefiance/node-express-suite-mongo:
UserService— User account operationsRoleService— Role and permission managementBackupCodeService— Backup code managementDatabaseInitializationService— Database initialization with default users and rolesMnemonicService— Mnemonic storage and retrievalDirectLoginTokenService— One-time login token managementRequestUserService— Extract user from request contextMongoBaseService— Mongoose-specific service base classMongoAuthenticationProvider— Mongoose-backed authentication
Utilities
debugLog()— Conditional logging utilitywithTransaction()— Database-agnostic transaction wrapper (IDatabase overload)
Testing
Testing Approach
The node-express-suite package uses comprehensive testing covering all services, middleware, controllers, and core operations.
Test Framework: Jest with TypeScript support Property-Based Testing: fast-check for validation properties Coverage: 57.86% overall, 100% on critical paths
Running Tests
# Run all tests
yarn nx test digitaldefiance-node-express-suite
# Run with coverage
yarn nx test digitaldefiance-node-express-suite --coverageTest Patterns
Testing Services
import { Application, JwtService } from '@digitaldefiance/node-express-suite';
describe('JwtService', () => {
let app: Application;
let jwtService: JwtService;
beforeAll(async () => {
// Set up your application with appropriate database plugin
app = new Application(environment, apiRouterFactory);
await app.start();
jwtService = new JwtService(app);
});
afterAll(async () => {
await app.stop();
});
it('should sign and verify token', async () => {
const { token } = await jwtService.signToken(user, app.environment.jwtSecret);
const verified = await jwtService.verifyToken(token);
expect(verified.userId).toBeDefined();
});
});Testing Middleware
import { authMiddleware } from '@digitaldefiance/node-express-suite';
import { Request, Response, NextFunction } from 'express';
describe('Auth Middleware', () => {
it('should reject requests without token', async () => {
const req = { headers: {} } as Request;
const res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
} as unknown as Response;
const next = jest.fn() as NextFunction;
await authMiddleware(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(next).not.toHaveBeenCalled();
});
});Testing Controllers
import { DecoratorBaseController } from '@digitaldefiance/node-express-suite';
describe('MyController', () => {
it('should handle requests', async () => {
// Test your controller endpoints
const req = { body: { /* ... */ } } as Request;
const result = await controller.handleRequest(req, res, next);
expect(result.statusCode).toBe(200);
});
});For testing Mongo-specific controllers (e.g.,
UserController), database operations, and model validation, see the testing documentation in@digitaldefiance/node-express-suite-mongo.
Testing Best Practices
- Mock external services like email providers
- Test error conditions and edge cases
- Test middleware in isolation and integration
- Test authentication and authorization flows
- Use property-based testing for validation logic
Cross-Package Testing
Testing integration with other Express Suite packages:
import { Application } from '@digitaldefiance/node-express-suite';
import { ECIESService } from '@digitaldefiance/node-ecies-lib';
describe('Cross-Package Integration', () => {
it('should integrate ECIES with application', async () => {
const app = new Application(environment, apiRouterFactory);
const ecies = new ECIESService();
// Test encryption/decryption within the application context
const mnemonic = ecies.generateNewMnemonic();
expect(mnemonic).toBeDefined();
});
});Decorator API
The decorator API provides a declarative, type-safe approach to building Express APIs with automatic OpenAPI documentation generation. Decorators eliminate boilerplate while maintaining full feature parity with manual RouteConfig.
Overview
| Category | Decorators | Purpose |
|----------|------------|---------|
| Controller | @Controller, @ApiController | Define controller base path and OpenAPI metadata |
| HTTP Methods | @Get, @Post, @Put, @Delete, @Patch | Define route handlers with OpenAPI support |
| Authentication | @RequireAuth, @RequireCryptoAuth, @Public, @AuthFailureStatus | Control authentication requirements |
| Parameters | @Param, @Body, @Query, @Header, @CurrentUser, @EciesUser, @Req, @Res, @Next | Inject request data into handler parameters |
| Validation | @ValidateBody, @ValidateParams, @ValidateQuery | Validate request data with Zod or express-validator |
| Response | @Returns, @ResponseDoc, @RawJson, @Paginated | Document response types for OpenAPI |
| Middleware | @UseMiddleware, @CacheResponse, @RateLimit | Attach middleware to routes |
| Transaction | @Transactional | Wrap handler in a database transaction |
| OpenAPI | @ApiOperation, @ApiTags, @ApiSummary, @ApiDescription, @Deprecated, @ApiOperationId, @ApiExample | Add OpenAPI documentation |
| OpenAPI Params | @ApiParam, @ApiQuery, @ApiHeader, @ApiRequestBody | Document parameters with full OpenAPI metadata |
| Lifecycle | @OnSuccess, @OnError, @Before, @After | Hook into request lifecycle events |
| Handler Args | @HandlerArgs | Pass additional arguments to handlers |
| Schema | @ApiSchema, @ApiProperty | Register OpenAPI schemas from classes |
Quick Start Example
import {
ApiController,
Get,
Post,
Put,
Delete,
RequireAuth,
Public,
Param,
Body,
Query,
ValidateBody,
Returns,
ApiTags,
Transactional,
DecoratorBaseController,
} from '@digitaldefiance/node-express-suite';
import { z } from 'zod';
// Define validation schema
const CreateUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(['admin', 'user']).optional(),
});
@RequireAuth() // All routes require authentication by default
@ApiTags('Users')
@ApiController('/api/users', {
description: 'User management endpoints',
})
class UserController extends DecoratorBaseController {
@Public() // Override class-level auth - this route is public
@Returns(200, 'User[]', { description: 'List of users' })
@Get('/')
async listUsers(
@Query('page', { schema: { type: 'integer' } }) page: number = 1,
@Query('limit') limit: number = 20,
) {
return this.userService.findAll({ page, limit });
}
@Returns(200, 'User', { description: 'User details' })
@Returns(404, 'ErrorResponse', { description: 'User not found' })
@Get('/:id')
async getUser(@Param('id', { description: 'User ID' }) id: string) {
return this.userService.findById(id);
}
@ValidateBody(CreateUserSchema)
@Transactional()
@Returns(201, 'User', { description: 'Created user' })
@Post('/')
async createUser(@Body() data: z.infer<typeof CreateUserSchema>) {
return this.userService.create(data);
}
@Transactional()
@Returns(200, 'User', { description: 'Updated user' })
@Put('/:id')
async updateUser(
@Param('id') id: string,
@Body() data: Partial<z.infer<typeof CreateUserSchema>>,
) {
return this.userService.update(id, data);
}
@Transactional()
@Returns(204, undefined, { description: 'User deleted' })
@Delete('/:id')
async deleteUser(@Param('id') id: string) {
await this.userService.delete(id);
}
}Controller Decorators
// Basic controller (no OpenAPI metadata)
@Controller('/api/items')
class ItemController {}
// OpenAPI-enabled controller with metadata
@ApiController('/api/users', {
tags: ['Users', 'Admin'],
description: 'User management endpoints',
deprecated: false,
name: 'UserController', // Optional, defaults to class name
})
class UserController extends DecoratorBaseController {}HTTP Method Decorators
All HTTP method decorators support inline OpenAPI options:
@Get('/users/:id', {
summary: 'Get user by ID',
description: 'Retrieves a user by their unique identifier',
tags: ['Users'],
operationId: 'getUserById',
deprecated: false,
auth: true, // Shorthand for @RequireAuth()
cryptoAuth: false, // Shorthand for @RequireCryptoAuth()
rawJson: false, // Shorthand for @RawJson()
transaction: false, // Shorthand for @Transactional()
middleware: [], // Express middleware array
validation: [], // express-validator chains
schema: zodSchema, // Zod schema for body validation
})
async getUser() {}Authentication Decorators
// Require JWT authentication
@RequireAuth()
@ApiController('/api/secure')
class SecureController {
@Get('/data')
getData() {} // Requires auth (inherited from class)
@Public()
@Get('/public')
getPublic() {} // No auth required (overrides class-level)
}
// Require ECIES crypto authentication
@RequireCryptoAuth()
@Post('/encrypted')
async createEncrypted() {}
// Custom auth failure status code
@AuthFailureStatus(403)
@Get('/admin')
getAdmin() {} // Returns 403 instead of 401 on auth failureParameter Injection Decorators
@Get('/:id')
async getUser(
// Path parameter with OpenAPI documentation
@Param('id', { description: 'User ID', schema: { type: 'string', format: 'uuid' } }) id: string,
// Query parameters
@Query('include', { description: 'Fields to include' }) include?: string,
// Header value
@Header('X-Request-ID') requestId?: string,
// Authenticated user from JWT
@CurrentUser() user: AuthenticatedUser,
// ECIES authenticated member
@EciesUser() member: EciesMember,
// Raw Express objects (use sparingly)
@Req() req: Request,
@Res() res: Response,
@Next() next: NextFunction,
) {}
@Post('/')
async createUser(
// Entire request body
@Body() data: CreateUserDto,
// Specific field from body
@Body('email') email: string,
) {}Validation Decorators
// Zod schema validation
const CreateUserSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
@ValidateBody(CreateUserSchema)
@Post('/')
async createUser(@Body() data: z.infer<typeof CreateUserSchema>) {}
// express-validator chains
@ValidateBody([
body('name').isString().notEmpty(),
body('email').isEmail(),
])
@Post('/')
async createUser() {}
// Language-aware validation with constants
@ValidateBody(function(lang) {
return [
body('username')
.matches(this.constants.UsernameRegex)
.withMessage(getTranslation(lang, 'invalidUsername')),
];
})
@Post('/')
async createUser() {}
// Validate path parameters
@ValidateParams(z.object({ id: z.string().uuid() }))
@Get('/:id')
async getUser() {}
// Validate query parameters
@ValidateQuery(z.object({
page: z.coerce.number().int().positive().optional(),
limit: z.coerce.number().int().max(100).optional(),
}))
@Get('/')
async listUsers() {}Response Decorators
// Document response types (stackable for multiple status codes)
@Returns(200, 'User', { description: 'User found' })
@Returns(404, 'ErrorResponse', { description: 'User not found' })
@Get('/:id')
async getUser() {}
// Inline schema for simple responses
@ResponseDoc(200, {
description: 'Health check response',
schema: {
type: 'object',
properties: {
status: { type: 'string' },
timestamp: { type: 'string', format: 'date-time' },
},
},
})
@Get('/health')
healthCheck() {}
// Raw JSON response (bypasses response wrapper)
@RawJson()
@Get('/raw')
getRawData() {}
// Paginated endpoint (adds page/limit query params to OpenAPI)
@Paginated({ defaultPageSize: 20, maxPageSize: 100 })
@Returns(200, 'User[]')
@Get('/')
async listUsers() {}
// Offset-based pagination
@Paginated({ useOffset: true, defaultPageSize: 20 })
@Get('/items')
async listItems() {}Middleware Decorators
// Attach middleware (class or method level)
@UseMiddleware(loggerMiddleware)
@ApiController('/api/data')
class DataController {
@UseMiddleware([validateMiddleware, sanitizeMiddleware])
@Post('/')
createData() {}
}
// Response caching
@CacheResponse({ ttl: 60 }) // Cache for 60 seconds
@Get('/static')
getStaticData() {}
@CacheResponse({
ttl: 300,
varyByUser: true, // Different cache per user
varyByQuery: ['page'], // Different cache per query param
keyPrefix: 'users', // Custom cache key prefix
})
@Get('/user-data')
getUserData() {}
// Rate limiting (auto-adds 429 response to OpenAPI)
@RateLimit({ requests: 5, window: 60 }) // 5 requests per minute
@Post('/login')
login() {}
@RateLimit({
requests: 100,
window: 3600,
byUser: true, // Limit per user instead of IP
message: 'Hourly limit exceeded',
keyGenerator: (req) => req.ip, // Custom key generator
})
@Get('/api-data')
getApiData() {}Transaction Decorator
// Basic transaction
@Transactional()
@Post('/')
async createOrder() {
// this.session available automatically in DecoratorBaseController
await this.orderService.create(data, this.session);
}
// Transaction with timeout
@Transactional({ timeout: 30000 }) // 30 second timeout
@Post('/bulk')
async bulkCreate() {}OpenAPI Operation Decorators
@ApiOperation({ summary: 'Get user by ID', description: 'Retrieves a user by their unique identifier', tags: ['Users'], operationId: 'getUserById', deprecated: false, }) @Get('/:id') getUser() {}
// Individual decorators (composable) @ApiSummary('Get user by ID') @ApiDescription('Retrieves a user by their unique identifier') @ApiTags('Users', 'Public') @ApiOperationId('getUserById') @Deprecated() @Get('/:id') getUser() {}
// Class-level tags apply to all methods @ApiTags('Users') @ApiController('/api/users') class UserController { @ApiTags('Admin') // Adds to class tags: ['Users', 'Admin'] @Get('/admin') adminEndpoint() {} }
// Add examples @ApiExample({ name: 'validUser', summary: 'A valid user response', value: { id: '123', name: 'John Doe', email: '[email protected]' }, type: 'response', statusCode: 200, }) @Get('/:id') getUser() {}
### OpenAPI Parameter Decorators
```typescript
// Document path parameter with full metadata
@ApiParam('id', {
description: 'User ID',
schema: { type: 'string', format: 'uuid' },
example: '123e4567-e89b-12d3-a456-426614174000',
})
@Get('/:id')
getUser(@Param('id') id: string) {}
// Document query parameters
@ApiQuery('page', {
description: 'Page number',
schema: { type: 'integer', minimum: 1 },
required: false,
example: 1,
})
@ApiQuery('sort', {
description: 'Sort field',
enum: ['name', 'date', 'id'],
})
@Get('/')
listUsers() {}
// Document headers
@ApiHeader('X-Request-ID', {
description: 'Request tracking ID',
schema: { type: 'string', format: 'uuid' },
required: true,
})
@Get('/')
getData() {}
// Document request body
@ApiRequestBody({
schema: 'CreateUserDto', // Reference to registered schema
description: 'User data to create',
required: true,
example: { name: 'John', email: '[email protected]' },
})
@Post('/')
createUser() {}
// Or with Zod schema
@ApiRequestBody({
schema: CreateUserSchema, // Zod schema
description: 'User data',
})
@Post('/')
createUser() {}Lifecycle Decorators
// Execute after successful response
@OnSuccess(({ req, result }) => {
console.log(`User ${req.params.id} fetched:`, result);
})
@Get('/:id')
getUser() {}
// Execute on error
@OnError(({ req, error }) => {
logger.error(`Error on ${req.path}:`, error);
})
@Get('/:id')
getUser() {}
// Execute before handler
@Before(({ req }) => {
console.log(`Incoming request to ${req.path}`);
})
@Get('/')
listUsers() {}
// Execute after handler (success or error)
@After(({ req, result, error }) => {
metrics.recordRequest(req.path, error ? 'error' : 'success');
})
@Get('/')
listUsers() {}
// Class-level hooks apply to all methods
@OnError(({ error }) => logger.error(error))
@ApiController('/api/users')
class UserController {}Handler Args Decorator
// Pass additional arguments to handler
@HandlerArgs({ maxItems: 100 })
@Get('/')
listItems(req: Request, config: { maxItems: number }) {
// config.maxItems === 100
}
// Multiple arguments
@HandlerArgs('prefix', 42, { option: true })
@Post('/')
createItem(req: Request, prefix: string, count: number, options: object) {}Schema Decorators
// Register class as OpenAPI schema
@ApiSchema({ description: 'User entity' })
class User {
@ApiProperty({
type: 'string',
format: 'uuid',
description: 'Unique identifier',
example: '123e4567-e89b-12d3-a456-426614174000',
})
id: string;
@ApiProperty({
type: 'string',
format: 'email',
required: true,
})
email: string;
@ApiProperty({
type: 'integer',
minimum: 0,
maximum: 150,
})
age?: number;
@ApiProperty({
type: 'array',
items: 'Role', // Reference to another schema
})
roles: Role[];
}
// Inheritance is supported
@ApiSchema()
class AdminUser extends User {
@ApiProperty({ type: 'string' })
adminLevel: string;
}Decorator Composition and Stacking
Decorators can be freely composed and stacked. Order matters for some decorators:
// Middleware executes top to bottom
@UseMiddleware(first)
@UseMiddleware(second)
@UseMiddleware(third)
@Get('/')
handler() {} // Executes: first → second → third → handler
// Multiple @Returns accumulate (don't replace)
@Returns(200, 'User')
@Returns(400, 'ValidationError')
@Returns(404, 'NotFoundError')
@Returns(500, 'ServerError')
@Get('/:id')
getUser() {}
// Tags merge (class + method)
@ApiTags('Users')
@ApiController('/api/users')
class UserController {
@ApiTags('Admin')
@Get('/admin')
admin() {} // Tags: ['Users', 'Admin']
}
// Method-level overrides class-level for same field
@RequireAuth()
@ApiController('/api/data')
class DataController {
@Get('/private')
private() {} // Requires auth (inherited)
@Public()
@Get('/public')
public() {} // No auth (overridden)
}Comparison: Manual RouteConfig vs Decorators
| RouteConfig Field | Decorator Equivalent |
|-------------------|---------------------|
| method | @Get, @Post, @Put, @Delete, @Patch |
| path | Decorator path argument |
| handlerKey | Decorated method name (automatic) |
| handlerArgs | @HandlerArgs(...args) |
| useAuthentication | @RequireAuth() |
| useCryptoAuthentication | @RequireCryptoAuth() |
| middleware | @UseMiddleware(...) |
| validation | @ValidateBody(), @ValidateParams(), @ValidateQuery() |
| rawJsonHandler | @RawJson() |
| authFailureStatusCode | @AuthFailureStatus(code) |
| useTransaction | @Transactional() |
| transactionTimeout | @Transactional({ timeout }) |
| openapi.summary | @ApiSummary(text) |
| openapi.description | @ApiDescription(text) |
| openapi.tags | @ApiTags(...tags) |
| openapi.operationId | @ApiOperationId(id) |
| openapi.deprecated | @Deprecated() |
| openapi.requestBody | @ApiRequestBody(options) |
| openapi.responses | @Returns(code, schema) |
| openapi.parameters | @ApiParam(), @ApiQuery(), @ApiHeader() |
Decorator Options TypeScript Types
// Controller options
interface ApiControllerOptions {
tags?: string[];
description?: string;
deprecated?: boolean;
name?: string;
}
// Route decorator options
interface RouteDecoratorOptions<TLanguage> {
validation?: ValidationChain[] | ((lang: TLanguage) => ValidationChain[]);
schema?: z.ZodSchema;
middleware?: RequestHandler[];
auth?: boolean;
cryptoAuth?: boolean;
rawJson?: boolean;
transaction?: boolean;
transactionTimeout?: number;
summary?: string;
description?: string;
tags?: string[];
operationId?: string;
deprecated?: boolean;
openapi?: OpenAPIRouteMetadata;
}
// Parameter decorator options
interface ParamDecoratorOptions {
description?: string;
example?: unknown;
required?: boolean;
schema?: OpenAPIParameterSchema;
}
// Cache options
interface CacheDecoratorOptions {
ttl: number; // Time to live in seconds
keyPrefix?: string;
varyByUser?: boolean;
varyByQuery?: string[];
}
// Rate limit options
interface RateLimitDecoratorOptions {
requests: number; // Max requests
window: number; // Time window in seconds
message?: string;
byUser?: boolean;
keyGenerator?: (req: Request) => string;
}
// Pagination options
interface PaginatedDecoratorOptions {
defaultPageSize?: number;
maxPageSize?: number;
useOffset?: boolean; // Use offset/limit instead of page/limit
}
// Transaction options
interface TransactionalDecoratorOptions {
timeout?: number; // Timeout in milliseconds
}Troubleshooting
Decorators not working?
- Ensure
experimentalDecorators: trueandemitDecoratorMetadata: truein tsconfig.json - Import
reflect-metadataat the top of your entry file - Extend
DecoratorBaseControllerfor full decorator support
OpenAPI metadata not appearing?
- Use
@ApiControllerinstead of@Controllerfor OpenAPI support - Ensure decorators are applied before HTTP method decorators (bottom-up execution)
- Check that schemas are registered with
@ApiSchemaorOpenAPISchemaRegistry
Authentication not enforced?
- Verify
@RequireAuth()is applied at class or method level - Check that
@Public()isn't overriding at method level - Ensure auth middleware is configured in your application
Validation errors not returning 400?
- Validation decorators automatically add 400 response to OpenAPI
- Ensure validation middleware is properly configured
- Check that Zod schemas or express-validator chains are valid
Parameter injection not working?
- Parameter decorators must be on method parameters, not properties
- Ensure the handler is called through the decorated controller
- Check parameter index matches the decorator position
For detailed migration instructions, see docs/DECORATOR_MIGRATION.md.
Documentation
📚 Comprehensive documentation is available in the docs/ directory.
Quick Links
- 📚 Documentation Index — Complete documentation index
- 🏗️ Architecture — System design and architecture
- 🎮 Controllers — Controller system and decorators
- ⚙️ Services — Business logic and service container
- 📊 Models — Database plugin interface
- 🔌 Middleware — Request pipeline
- � Mongo Split Migration — Migrating to the two-package architecture
See the full documentation index for all available documentation.
License
MIT © Digital Defiance
Related Packages
@digitaldefiance/node-express-suite-mongo— MongoDB/Mongoose plugin for this package@digitaldefiance/ecies-lib— Core ECIES encryption library@digitaldefiance/node-ecies-lib— Node.js ECIES implementation@digitaldefiance/i18n-lib— Internationalization framework@digitaldefiance/suite-core-lib— Core user management primitives
Contributing
Contributions are welcome! Please read the contributing guidelines in the main repository.
Support
For issues and questions:
- GitHub Issues: https://github.com/Digital-Defiance/node-express-suite/issues
- Email: [email protected]
Plugin-Based Architecture
The framework uses a plugin-based architecture that separates database concerns from the core application. This replaces the old deep inheritance hierarchy (Mongo base → Application → concrete subclass) with a composable plugin pattern.
Architecture Overview
BaseApplication<TID> ← Database-agnostic base (accepts IDatabase)
└── Application<TID> ← HTTP/Express layer (server, routing, middleware)
└── useDatabasePlugin() ← Plug in any database backend
IDatabasePlugin<TID> ← Plugin interface for database backends
└── MongoDatabasePlugin ← Mongoose/MongoDB implementation (in node-express-suite-mongo)
MongoApplicationConcrete ← Ready-to-use concrete class (in node-express-suite-mongo)Core Classes
| Class | Purpose |
|-------|---------|
| BaseApplication<TID> | Database-agnostic base. Accepts an IDatabase instance and optional lifecycle hooks. Manages PluginManager, ServiceContainer, and environment. |
| Application<TID> | Extends BaseApplication with Express HTTP/HTTPS server, routing, CSP/Helmet config, and middleware. Database-agnostic — database backends are provided via IDatabasePlugin. |
| MongoApplicationConcrete<TID> | Concrete Application subclass for testing/development (in node-express-suite-mongo). Wires up MongoDatabasePlugin with default configuration, schema maps, and a dummy email service. |
IDatabasePlugin Interface
The IDatabasePlugin<TID> interface extends IApplicationPlugin<TID> with database-specific lifecycle hooks:
interface IDatabasePlugin<TID> extends IApplicationPlugin<TID> {
readonly database: IDatabase;
readonly authenticationProvider?: IAuthenticationProvider<TID>;
connect(uri?: string): Promise<void>;
disconnect(): Promise<void>;
isConnected(): boolean;
// Optional dev/test store management
setupDevStore?(): Promise<string>;
teardownDevStore?(): Promise<void>;
initializeDevStore?(): Promise<unknown>;
}MongoDatabasePlugin
Available in
@digitaldefiance/node-express-suite-mongo.
MongoDatabasePlugin implements IDatabasePlugin for MongoDB/Mongoose:
import { MongoDatabasePlugin } from '@digitaldefiance/node-express-suite-mongo';
const mongoPlugin = new MongoDatabasePlugin({
schemaMapFactory: getSchemaMap,
databaseInitFunction: DatabaseInitializationService.initUserDb.bind(DatabaseInitializationService),
initResultHashFunction: DatabaseInitializationService.serverInitResultHash.bind(DatabaseInitializationService),
environment,
constants,
});It wraps a MongooseDocumentStore and provides:
- Connection/disconnection lifecycle
- Dev database provisioning via
MongoMemoryReplSet - Authentication provider wiring
- Mongoose model and schema map access
Application Constructor
The Application constructor accepts these parameters:
constructor(
environment: TEnvironment,
apiRouterFactory: (app: IApplication<TID>) => BaseRouter<TID>,
cspConfig?: ICSPConfig | HelmetOptions | IFlexibleCSP,
constants?: TConstants,
appRouterFactory?: (apiRouter: BaseRouter<TID>) => TAppRouter,
customInitMiddleware?: typeof initMiddleware,
database?: IDatabase, // Optional — use useDatabasePlugin() instead
)The database parameter is optional. When using a database plugin, the plugin's database property is used automatically.
Registering a Database Plugin
Use useDatabasePlugin() to register a database plugin with the application:
import { Application } from '@digitaldefiance/node-express-suite';
import { MongoDatabasePlugin } from '@digitaldefiance/node-express-suite-mongo';
const app = new Application(environment, apiRouterFactory);
app.useDatabasePlugin(mongoPlugin);
await app.start();useDatabasePlugin() stores the plugin as the application's database plugin AND registers it with the PluginManager, so it participates in the full plugin lifecycle (init, stop).
Implementing a Custom Database Plugin (BrightChain Example)
To use a non-Mongo database, implement IDatabasePlugin directly. You do not need IDocumentStore or any Mongoose types:
import type { IDatabasePlugin, IDatabase, IApplication } from '@digitaldefiance/node-express-suite';
class BrightChainDatabasePlugin implements IDatabasePlugin<Buffer> {
readonly name = 'brightchain-database';
readonly version = '1.0.0';
private _connected = false;
private _database: IDatabase;
constructor(private config: BrightChainConfig) {
this._database = new BrightChainDatabase(config);
}
get database(): IDatabase {
return this._database;
}
// Optional: provide an auth provider if your DB manages authentication
get authenticationProvider() {
return undefined;
}
async connect(uri?: string): Promise<void> {
await this._database.connect(uri ?? this.config.connectionString);
this._connected = true;
}
async disconnect(): Promise<void> {
await this._database.disconnect();
this._connected = false;
}
isConnected(): boolean {
return this._connected;
}
async init(app: IApplication<Buffer>): Promise<void> {
// Wire up any app-level integrations after connection
// e.g., register services, set auth provider, etc.
}
async stop(): Promise<void> {
await this.disconnect();
}
}
// Usage:
const app = new Application(environment, apiRouterFactory);
app.useDatabasePlugin(new BrightChainDatabasePlugin(config));
await app.start();Migration Guide: Inheritance → Plugin Architecture
This section covers migrating from the old inheritance-based hierarchy to the new plugin-based architecture.
What Changed
| Before | After |
|--------|-------|
| Old Mongo base → Application → old concrete class | BaseApplication → Application + IDatabasePlugin |
| Database logic baked into the class hierarchy | Database logic provided via plugins |
| Old concrete class for testing/dev | MongoApplicationConcrete for testing/dev |
| Extending the old Mongo base for custom apps | Implementing IDatabasePlugin for custom databases |
Before (Old Hierarchy)
// Old: The concrete class extended Application which extended the Mongo base class
// Database logic was tightly coupled into the inheritance chain
import { /* old concrete class */ } from '@digitaldefiance/node-express-suite';
const app = new OldConcreteApp(environment);
await app.start();After (Plugin Architecture)
// New: Application is database-agnostic, MongoDatabasePlugin provides Mongo support
import { Application } from '@digitaldefiance/node-express-suite';
import { MongoApplicationConcrete } from '@digitaldefiance/node-express-suite-mongo';
// For testing/development (drop-in replacement for the old concrete class):
const app = new MongoApplicationConcrete(environment);
await app.start();Or for custom wiring:
import { Application } from '@digitaldefiance/node-express-suite';
import {
MongoDatabasePlugin,
DatabaseInitializationService,
getSchemaMap,
} from '@digitaldefiance/node-express-suite-mongo';
const app = new Application(environment, apiRouterFactory);
const mongoPlugin = new MongoDatabasePlugin({
schemaMapFactory: getSchemaMap,
databaseInitFunction: DatabaseInitializationService.initUserDb.bind(DatabaseInitializationService),
initResultHashFunction: DatabaseInitializationService.serverInitResultHash.bind(DatabaseInitializationService),
environment,
constants,
});
app.useDatabasePlugin(mongoPlugin);
await app.start();Key Renames
| Old Name | New Name | Notes |
|----------|----------|-------|
| Old concrete class | MongoApplicationConcrete (in node-express-suite-mongo) | Drop-in replacement for testing/dev |
| Old Mongo base class | (removed) | Functionality moved to BaseApplication + MongoDatabasePlugin |
| Old base file | base-application.ts | File renamed |
| Old concrete file | mongo-application-concrete.ts | File renamed |
Migration Checklist
- [ ] Replace the old concrete class with
MongoApplicationConcrete(fromnode-express-suite-mongo) - [ ] Replace any old Mongo base subclasses with
Application+useDatabasePlugin() - [ ] Update imports to use
base-application(renamed from old base file) - [ ] Update imports to use
mongo-application-concrete(renamed from old concrete file) - [ ] If you had a custom concrete subclass, convert it to use
MongoDatabasePluginor implement your ownIDatabasePlugin
Architecture Refactor (2025)
Major improvements with large complexity reduction:
New Features
Service Container
// Centralized dependency injection
const jwtService = app.services.get(ServiceKeys.JWT);Simplified Generics
// Before: IApplication<T, I, TBaseDoc, TEnv, TConst, ...>
// After: IApplication
const app: IApplication = ...;Validation Builder
validation: function(lang) {
return ValidationBuilder.create(lang, this.constants)
.for('email').isEmail().withMessage(key)
.for('username').matches(c => c.UsernameRegex).withMessage(key)
.build();
}Transaction Decorator
@Post('/register', { transaction: true })
async register() {
// this.session available automatically
await this.userService.create(data, this.session);
}Response Builder
return Response.created()
.message(SuiteCoreStringKey.Registration_Success)
.data({ user, mnemonic })
.build();Plugin System
class MyPlugin implements IApplicationPlugin {
async init(app: IApplication) { /* setup */ }
async stop() { /* cleanup */ }
}
app.plugins.register(new MyPlugin());Route Builder DSL
RouteBuilder.create()
.post('/register')
.auth()
.validate(validation)
.transaction()
.handle(this.register);Migration Guide (v1.x → v2.0)
Overview
Version 2.0 introduces a major architecture refactor with 50% complexity reduction while maintaining backward compatibility where possible. This guide helps you migrate from v1.x to v2.0.
Breaking Changes
1. Simplified Generic Parameters
Before (v1.x):
class Application<T, I, TInitResults, TModelDocs, TBaseDocument, TEnvironment, TConstants, TAppRouter>
class UserController<I, D, S, A, TUser, TTokenRole, TTokenUser, TApplication, TLanguage>After (v2.0):
class Application // No generic parameters
class UserController<TConfig extends ControllerConfig, TLanguage>Migration:
- Remove all generic type parameters from Application instantiation
- Update controller signatures to use ControllerConfig interface
- Type information now inferred from configuration objects
2. Service Instantiation
Before (v1.x):
const jwtService = new JwtService(app);After (v2.0):
const jwtService = app.services.get(ServiceKeys.JWT);Migration:
- Replace direct service instantiation with container access
- Services are now singletons managed by the container
- Import ServiceKeys enum for type-safe service access
- Note:
UserService,RoleService, and other Mongo-specific services are now in@digitaldefiance/node-express-suite-mongo
Recommended Migrations (Non-Breaking)
3. Transaction Handling
Before (v1.x):
async register(req: Request, res: Response, next: NextFunction) {
return await withTransaction(
this.application.db.connection,
this.application.environment.mongo.useTransactions,
undefined,
async (session) => {
const user = await this.userService.create(data, session);
const mnemonic = await this.mnemonicService.store(userId, session);
return { statusCode: 201, response: { user, mnemonic } };
}
);
}After (v2.0):
@Post('/register', { transaction: true })
async register(req: Request, res: Response, next: NextFunction) {
const user = await this.userService.create(data, this.session);
const mnemonic = await this.mnemonicService.store(userId, this.session);
return Response.created().data({ user, mnemonic }).build();
}Benefits:
- 70% reduction in transaction boilerplate
- Automatic session management
- Cleaner, more readable code
4. Response Construction
Before (v1.x):
return {
statusCode: 201,
response: {
message: getSuiteCoreTranslation(SuiteCoreStringKey.Registration_Success, undefined, lang),
data: { user, mnemonic }
}
};After (v2.0):
return Response.created()
.message(SuiteCoreStringKey.Registration_Success)
.data({ user, mnemonic })
.build();Benefits:
- 40% reduction in response boilerplate
- Fluent, chainable API
- Automatic translation handling
5. Validation
Before (v1.x):
protected getValidationRules(lang: TLanguage) {
return [
body('username')
.matches(this.constants.UsernameRegex)
.withMessage(getSuiteCoreTranslation(key, undefined, lang)),
body('email')
.isEmail()
.withMessage(getSuiteCoreTranslation(key, undefined, lang))
];
}After (v2.0):
validation: function(lang: TLanguage) {
return ValidationBuilder.create(lang, this.constants)
.for('username').matches(c => c.UsernameRegex).withMessage(key)
.for('email').isEmail().withMessage(key)
.build();
}Benefits:
- 50% reduction in validation code
- Constants automatically injected
- Type-safe field access
- Cleaner syntax
6. Middleware Composition
Before (v1.x):
router.post('/backup-codes',
authMiddleware,
authenticateCryptoMiddleware,
validateSchema(backupCodeSchema),
this.getBackupCodes.bind(this)
);After (v2.0):
@Post('/backup-codes', {
pipeline: Pipeline.create()
.use(Auth.token())
.use(Auth.crypto())
.use(Validate.schema(backupCodeSchema))
.build()
})
async getBackupCodes() { /* ... */ }Benefits:
- Explicit middleware ordering
- Reusable pipeline definitions
- Better readability
Step-by-Step Migration
Step 1: Update Dependencies
npm install @digitaldefiance/node-express-suite@^2.0.0
# or
yarn add @digitaldefiance/node-express-suite@^2.0.0Step 2: Update Application Initialization
Before:
const app = new Application<MyTypes, MyIds, MyResults, MyModels, MyDoc, MyEnv, MyConst, MyRouter>({
port: 3000,
jwtSecret: process.env.JWT_SECRET
});After:
const app = new Application(environment, apiRouterFactory);
// If using MongoDB, register the plugin from node-express-suite-mongo:
// app.useDatabasePlugin(mongoPlugin);Step 3: Update Service Access
Find and replace service instantiation:
# Find
new JwtService(app)
# Replace with
app.services.get(ServiceKeys.JWT)Note:
UserService,RoleService, and other Mongo-specific services are now in@digitaldefiance/node-express-suite-mongo. Update those imports accordingly.
Step 4: Migrate Controllers (Gradual)
Start with high-traffic endpoints:
- Add transaction decorator to write operations
- Replace response construction with Response builder
- Update validation to use ValidationBuilder
- Migrate middleware to Pipeline builder
Step 5: Test Thoroughly
# Run full test suite
yarn nx test digitaldefiance-node-express-suite
# Run specific test suites
yarn nx test digitaldefiance-node-express-suite --testPathPatterns="jwt"
# Check for deprecation warnings
DEBUG=* yarn startMigration Checklist
- [ ] Update package to v2.0.0
- [ ] Remove generic parameters from Application
- [ ] Update service instantiation to use container
- [ ] Migrate transaction handling (high-priority endpoints)
- [ ] Migrate response construction (high-priority endpoints)
- [ ] Update validation rules (new endpoints first)
- [ ] Migrate middleware composition (optional)
- [ ] Run full test suite
- [ ] Check for deprecation warnings
- [ ] Update documentation
- [ ] Deploy to staging
- [ ] Monitor for issues
- [ ] Deploy to production
Backward Compatibility
The following v1.x patterns still work in v2.0:
✅ Direct service instantiation (with deprecation warning) ✅ Manual transaction wrapping with withTransaction ✅ Manual response construction ✅ Traditional validation rules ✅ Direct middleware composition
Performance Considerations
- Service container adds negligible overhead (~0.1ms per request)
- Transaction decorator has same performance as manual wrapping
- Response builder is optimized for common cases
- Validation builder compiles to same express-validator chains
Troubleshooting
Issue: Type errors after upgrade
Solution: Remove generic type parameters from Application and controller signatures.
Issue: Services not found in container
Solution: Ensure services are registered during application initialization. Check ServiceKeys enum.
Issue: Transaction session undefined
Solution: Add { transaction: true } to route decorator options.
Issue: Validation not working
Solution: Ensure ValidationBuilder.create receives correct language and constants.
Getting Help
- Documentation: See docs/INDEX.md for the complete documentation index
- Migration: See docs/MONGO_SPLIT_MIGRATION.md for the package split migration
- Issues: Report bugs at GitHub Issues
- Support: Email [email protected]
Additional Resources
- Mongo Split Migration
- [Decorator
