@digitaldefiance/node-express-suite
v4.2.1
Published
Generic express application and routing library with decorator support
Downloads
6,649
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.
It is an 'out of the box' solution with a specific recipe (Mongo, Express, React, Node, (MERN) stack) with ejs templating, 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.
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
- 📊 Dynamic Model Registry: Extensible document model system
- 🔧 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
- 💾 MongoDB Integration: Full database layer with Mongoose schemas
- 🧪 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
Installation
npm install @digitaldefiance/node-express-suite
# or
yarn add @digitaldefiance/node-express-suiteQuick Start
Basic Server Setup
import { Application, DatabaseInitializationService, emailServiceRegistry } 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({
port: 3000,
mongoUri: 'mongodb://localhost:27017/myapp',
jwtSecret: process.env.JWT_SECRET,
defaultLanguage: LanguageCodes.EN_US
});
// Configure email service (required before using middleware)
emailServiceRegistry.setService(new EmailService(app));
// Initialize database with default users and roles
const initResult = await DatabaseInitializationService.initUserDb(app);
// Start server
await app.start();
console.log(`Server running on port ${app.environment.port}`);User Authentication
import { JwtService, UserService } from '@digitaldefiance/node-express-suite';
// Create services
const jwtService = new JwtService(app);
const userService = new UserService(app);
// Sign in user
const user = await userService.findByUsername('alice');
const { token, roles } = await jwtService.signToken(user, app.environment.jwtSecret);
// Verify token
const tokenUser = await jwtService.verifyToken(token);
console.log(`User ${tokenUser.userId} authenticated with roles:`, tokenUser.roles);Core Components
Dynamic Model Registry
The package uses a dynamic model registration system for extensibility:
import { ModelRegistry } from '@digitaldefiance/node-express-suite';
// Register a custom model
ModelRegistry.instance.register({
modelName: 'Organization',
schema: organizationSchema,
model: OrganizationModel,
collection: 'organizations',
});
// Retrieve model anywhere in your app
const OrgModel = ModelRegistry.instance.get<IOrganizationDocument>('Organization').model;
// Use the model
const org = await OrgModel.findById(orgId);Built-in Models
The framework includes these pre-registered models:
- User: User accounts with authentication
- Role: Permission roles for RBAC
- UserRole: User-to-role associations
- EmailToken: Email verification and recovery tokens
- Mnemonic: Encrypted mnemonic storage
- UsedDirectLoginToken: One-time login token tracking
Extending Models and Schemas
All model functions support generic type parameters for custom model names and collections:
import { UserModel, EmailTokenModel } from '@digitaldefiance/node-express-suite';
// Use with default enums
const defaultUserModel = UserModel(connection);
// Use with custom model names and collections
const customUserModel = UserModel(
connection,
'CustomUser',
'custom_users'
);Extending Schemas
Clone and extend base schemas with additional fields:
import { EmailTokenSchema } from '@digitaldefiance/node-express-suite';
import { Schema } from 'mongoose';
// Clone and extend the schema
const ExtendedEmailTokenSchema = EmailTokenSchema.clone();
ExtendedEmailTokenSchema.add({
customField: { type: String, required: false },
metadata: { type: Schema.Types.Mixed, required: false },
});
// Use with custom model
const MyEmailTokenModel = connection.model(
'ExtendedEmailToken',
ExtendedEmailTokenSchema,
'extended_email_tokens'
);Extending Model Functions
Create custom model functions that wrap extended schemas:
import { IEmailTokenDocument } from '@digitaldefiance/node-express-suite';
import { Connection, Model } from 'mongoose';
// Extend the document interface
interface IExtendedEmailTokenDocument extends IEmailTokenDocument {
customField?: string;
metadata?: any;
}
// Create extended schema (as shown above)
const ExtendedEmailTokenSchema = EmailTokenSchema.clone();
ExtendedEmailTokenSchema.add({
customField: { type: String },
metadata: { type: Schema.Types.Mixed },
});
// Create custom model function
export function ExtendedEmailTokenModel<
TModelName extends string = 'ExtendedEmailToken',
TCollection extends string = 'extended_email_tokens'
>(
connection: Connection,
modelName: TModelName = 'ExtendedEmailToken' as TModelName,
collection: TCollection = 'extended_email_tokens' as TCollection,
): Model<IExtendedEmailTokenDocument> {
return connection.model<IExtendedEmailTokenDocument>(
modelName,
ExtendedEmailTokenSchema,
collection,
);
}
// Use the extended model
const model = ExtendedEmailTokenModel(connection);
const token = await model.create({
userId,
type: EmailTokenType.AccountVerification,
token: 'abc123',
email: '[email protected]',
customField: 'custom value',
metadata: { source: 'api' },
});Custom Enumerations
Extend the base enumerations for your application:
import { BaseModelName, SchemaCollection } from '@digitaldefiance/node-express-suite';
// Extend base enums
enum MyModelName {
User = BaseModelName.User,
Role = BaseModelName.Role,
Organization = 'Organization',
Project = 'Project',
}
enum MyCollection {
User = SchemaCollection.User,
Role = SchemaCollection.Role,
Organization = 'organizations',
Project = 'projects',
}
// Use with model functions
const orgModel = UserModel<MyModelName, MyCollection>(
connection,
MyModelName.Organization,
MyCollection.Organization
);Complete Extension Example
Combining schemas, documents, and model functions:
import { IUserDocument, UserSchema } from '@digitaldefiance/node-express-suite';
import { Connection, Model, Schema } from 'mongoose';
// 1. Extend document interface
interface IOrganizationUserDocument extends IUserDocument {
organizationId: string;
department?: string;
}
// 2. Extend schema
const OrganizationUserSchema = UserSchema.clone();
OrganizationUserSchema.add({
organizationId: { type: String, required: true },
department: { type: String },
});
// 3. Create model function
export function OrganizationUserModel(
connection: Connection,
): Model<IOrganizationUserDocument> {
return connection.model<IOrganizationUserDocument>(
'OrganizationUser',
OrganizationUserSchema,
'organization_users',
);
}
// 4. Use in application
const model = OrganizationUserModel(connection);
const user = await model.create({
username: 'alice',
email: '[email protected]',
organizationId: 'org-123',
department: 'Engineering',
});Services
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
Role and permission management:
import { RoleService } from '@digitaldefiance/node-express-suite';
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
Backup code generation and validation:
import { BackupCodeService } from '@digitaldefiance/node-express-suite';
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
Initialize database with default users and roles:
import { DatabaseInitializationService } from '@digitaldefiance/node-express-suite';
// 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 { emailServiceRegistry, 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
emailServiceRegistry.setService(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:
const app = new Application({ jwtSecret: process.env.JWT_SECRET, mongoUri: process.env.MONGO_URI, });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:
userSchema.index({ email: 1 }, { unique: true }); userSchema.index({ username: 1 }, { unique: true });
API Reference
Application
new Application(config)- Create application instancestart()- Start the Express serverstop()- Stop the server gracefullyenvironment- Access configuration
Services
ECIESService- Encryption and key managementKeyWrappingService- Secure key storageJwtService- JWT token operationsRoleService- Role and permission managementUserService- User account operationsBackupCodeService- Backup code managementMnemonicService- Mnemonic storage and retrievalSystemUserService- System user operationsDatabaseInitializationService- Database initialization with default users and rolesDirectLoginTokenService- One-time login token managementRequestUserService- Extract user from request contextChecksumService- CRC checksum operationsSymmetricService- Symmetric encryption operationsXorService- XOR cipher operationsFecService- Forward error correctionDummyEmailService- Test email service implementation
Utilities
ModelRegistry- Dynamic model registrationdebugLog()- Conditional logging utilitywithTransaction()- MongoDB transaction wrapper
Testing
Testing Approach
The node-express-suite package uses comprehensive testing with 604 tests covering all services, middleware, controllers, and database operations.
Test Framework: Jest with TypeScript support
Property-Based Testing: fast-check for validation properties
Coverage: 57.86% overall, 100% on critical paths
Database: MongoDB Memory Server for isolated testing
Test Structure
tests/
├── unit/ # Unit tests for services and utilities
├── integration/ # Integration tests for multi-service flows
├── e2e/ # End-to-end API tests
├── middleware/ # Middleware tests
└── fixtures/ # Test data and mocksRunning Tests
# Run all tests
npm test
# Run with coverage
npm test -- --coverage
# Run specific test suite
npm test -- user-service.spec.ts
# Run in watch mode
npm test -- --watchTest Patterns
Testing Services
import { UserService, Application } from '@digitaldefiance/node-express-suite';
describe('UserService', () => {
let app: Application;
let userService: UserService;
beforeAll(async () => {
app = new Application({
mongoUri: 'mongodb://localhost:27017/test',
jwtSecret: 'test-secret'
});
await app.start();
userService = new UserService(app);
});
afterAll(async () => {
await app.stop();
});
it('should create user', async () => {
const user = await userService.create({
username: 'alice',
email: '[email protected]',
password: 'SecurePass123!'
});
expect(user.username).toBe('alice');
});
});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 { UserController } from '@digitaldefiance/node-express-suite';
describe('UserController', () => {
it('should register new user', async () => {
const controller = new UserController(app);
const req = {
body: {
username: 'alice',
email: '[email protected]',
password: 'SecurePass123!'
}
} as Request;
const result = await controller.register(req, res, next);
expect(result.statusCode).toBe(201);
expect(result.response.data.user).toBeDefined();
});
});Testing Database Operations
import { connectMemoryDB, disconnectMemoryDB, clearMemoryDB } from '@digitaldefiance/express-suite-test-utils';
import { UserModel } from '@digitaldefiance/node-express-suite';
describe('User Model', () => {
beforeAll(async () => {
await connectMemoryDB();
});
afterAll(async () => {
await disconnectMemoryDB();
});
afterEach(async () => {
await clearMemoryDB();
});
it('should validate user schema', async () => {
const User = UserModel(connection);
const user = new User({
username: 'alice',
email: '[email protected]'
});
await expect(user.validate()).resolves.not.toThrow();
});
});Testing Best Practices
- Use MongoDB Memory Server for isolated database testing
- Test with transactions to ensure data consistency
- Mock external services like email providers
- Test error conditions and edge cases
- Test middleware in isolation and integration
- Test authentication and authorization flows
Cross-Package Testing
Testing integration with other Express Suite packages:
import { Application } from '@digitaldefiance/node-express-suite';
import { ECIESService } from '@digitaldefiance/node-ecies-lib';
import { IBackendUser } from '@digitaldefiance/suite-core-lib';
describe('Cross-Package Integration', () => {
it('should integrate ECIES with user management', async () => {
const app = new Application({ /* config */ });
const ecies = new ECIESService();
// Create user with encrypted data
const user = await app.services.get(ServiceKeys.USER).create({
username: 'alice',
email: '[email protected]',
// ... encrypted fields
});
expect(user).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 handlers in MongoDB transactions |
| 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
// Full operation metadata
@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
// 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 - Data models and registry
- 🔌 Middleware - Request pipeline
- 💾 Transactions - Transaction management
- 🔧 Plugins - Plugin system
See the full documentation index for all available documentation.
License
MIT © Digital Defiance
Related Packages
@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
MongoApplicationConcrete ← Ready-to-use concrete class for testing/devCore 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. Wires up MongoDatabasePlugin with default configuration, schema maps, and a dummy email service. Replaces the old concrete class. |
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
MongoDatabasePlugin implements IDatabasePlugin for MongoDB/Mongoose:
import { MongoDatabasePlugin } from '@digitaldefiance/node-express-suite';
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:
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 { MongoApplicationConcrete } from '@digitaldefiance/node-express-suite';
// 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, MongoDatabasePlugin } from '@digitaldefiance/node-express-suite';
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 | 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 - [ ] 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);
const userService = app.services.get(ServiceKeys.USER);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);
const userService = new UserService(app);
const roleService = new RoleService(app);After (v2.0):
const jwtService = app.services.get(ServiceKeys.JWT);
const userService = app.services.get(ServiceKeys.USER);
const roleService = app.services.get(ServiceKeys.ROLE);Migration:
- Replace direct service instantiation with container access
- Services are now singletons managed by the container
- Import ServiceKeys enum for type-safe service access
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,
mongoUri: process.env.MONGO_URI,
jwtSecret: process.env.JWT_SECRET
});**After:
