feat: middleware registry pattern for scalable middleware management
Some checks failed
armco-org/node-starter-kit/pipeline/head There was a failure building this commit

- Add MiddlewareFactory registry (similar to PluginFactory)
- Middlewares now self-register with name, category, and order
- Auto-initialization from armcorc.json via registry
- Consistent [NSK][MIDDLEWARE] logging handled by registry
- Reduce new middleware addition from 4 places to 1-2 places
- Fix tsconfig.json ignoreDeprecations build error

Breaking: No breaking changes - backward compatible
Middlewares: bodyParser, cookieParser, helmet, cors, rateLimit all self-register
This commit is contained in:
2025-12-16 23:14:41 +05:30
parent 411f63d79f
commit 9193fdb943
10 changed files with 362 additions and 74 deletions

View File

@@ -2,7 +2,6 @@
"compilerOptions": {
"declaration": true,
"declarationDir": "dist",
"ignoreDeprecations": "6.0",
"target": "ES2020",
"module": "commonjs",
"moduleResolution": "node",

View File

@@ -276,12 +276,8 @@ export class ApplicationBuilder {
* Build and return the application
*/
async build(): Promise<Application> {
// Import middleware initializers
const { initHelmet } = await import('../middlewares/security/helmet')
const { initCors } = await import('../middlewares/security/cors')
const { initRateLimiter } = await import('../middlewares/security/rateLimiter')
const { initCookieParser } = await import('../middlewares/utils/cookieParser')
const { initBodyParser } = await import('../middlewares/utils/bodyParser')
// Import middleware registry (this triggers all middleware self-registrations)
const { middlewareRegistry } = await import('../core/MiddlewareFactory')
// Load configuration
let finalConfig: AppConfig
@@ -363,32 +359,10 @@ export class ApplicationBuilder {
// Auto-inject middlewares from config (AFTER plugins so logger is available)
const logger = application.getContainer().tryResolve<any>('logger')
// Inject middlewares in order (IMPORTANT: body parsers first, then security, then auth)
// Initialize all configured middlewares using registry
// The registry automatically handles order, logging, and initialization
if (finalConfig.middlewares) {
// 1. Body parser (must be early to parse request bodies)
if (finalConfig.middlewares.bodyParser) {
initBodyParser(this.app, finalConfig.middlewares.bodyParser as any, logger)
}
// 2. Cookie parser (must be early to parse cookies)
if (finalConfig.middlewares.cookieParser) {
initCookieParser(this.app, finalConfig.middlewares.cookieParser as any, logger)
}
// 3. Security middlewares
if (finalConfig.middlewares.helmet) {
initHelmet(this.app, finalConfig.middlewares.helmet as any, logger)
}
if (finalConfig.middlewares.cors) {
initCors(this.app, finalConfig.middlewares.cors as any, logger)
}
// 4. Rate limiting (after CORS)
const rateLimitConfig = finalConfig.middlewares.rateLimit || finalConfig.middlewares.rateLimiter
if (rateLimitConfig) {
initRateLimiter(this.app, rateLimitConfig as any, logger)
}
middlewareRegistry.initializeFromConfig(this.app, finalConfig.middlewares, logger)
}
return application

View File

@@ -0,0 +1,128 @@
import { Application } from 'express'
import { Logger } from '../types/Logger'
/**
* Middleware initializer function type
* Takes app, config, and logger, and registers the middleware
*/
export type MiddlewareInitializer = (app: Application, config: any, logger?: Logger) => void
/**
* Middleware registration metadata
*/
export interface MiddlewareMetadata {
name: string
category: 'security' | 'utils' | 'auth' | 'custom'
order: number // Lower = earlier in middleware chain
initializer: MiddlewareInitializer
}
/**
* Global registry for middleware initializers
* Maps middleware names (from config) to their initializer functions
*/
class MiddlewareFactoryRegistry {
private middlewares = new Map<string, MiddlewareMetadata>()
/**
* Register a middleware initializer
* @param metadata - Middleware metadata including name and initializer function
*/
register(metadata: MiddlewareMetadata): void {
this.middlewares.set(metadata.name, metadata)
}
/**
* Get a middleware by name
*/
get(name: string): MiddlewareMetadata | undefined {
return this.middlewares.get(name)
}
/**
* Check if a middleware is registered
*/
has(name: string): boolean {
return this.middlewares.has(name)
}
/**
* Get all registered middleware names
*/
getRegisteredNames(): string[] {
return Array.from(this.middlewares.keys())
}
/**
* Get middlewares sorted by execution order
*/
getSortedMiddlewares(): MiddlewareMetadata[] {
return Array.from(this.middlewares.values()).sort((a, b) => a.order - b.order)
}
/**
* Initialize middlewares from config
* @param app - Express application
* @param middlewaresConfig - Middleware configuration from AppConfig
* @param logger - Optional logger instance
*/
initializeFromConfig(
app: Application,
middlewaresConfig: Record<string, any>,
logger?: Logger
): void {
// Get middlewares sorted by order
const sortedMiddlewares = this.getSortedMiddlewares()
for (const middleware of sortedMiddlewares) {
const config = middlewaresConfig[middleware.name]
// Skip if not configured
if (!config) {
continue
}
// Skip if explicitly disabled
if (config.enabled === false) {
logger?.debug(`[NSK][${middleware.name.toUpperCase()}] Middleware disabled`)
continue
}
try {
logger?.info(`[NSK][${middleware.name.toUpperCase()}] Initializing...`)
middleware.initializer(app, config, logger)
logger?.info(`[NSK][${middleware.name.toUpperCase()}] Initialized`)
} catch (error) {
logger?.error(`[NSK][${middleware.name.toUpperCase()}] Failed to initialize:`, error)
throw new Error(
`Middleware initialization failed for '${middleware.name}': ${
error instanceof Error ? error.message : String(error)
}`
)
}
}
}
}
/**
* Global middleware factory registry instance
*/
export const middlewareRegistry = new MiddlewareFactoryRegistry()
/**
* Register a middleware initializer (convenience function)
*
* @example
* ```typescript
* // In your middleware file:
* registerMiddleware({
* name: 'cors',
* category: 'security',
* order: 30,
* initializer: initCors
* })
* ```
*/
export function registerMiddleware(metadata: MiddlewareMetadata): void {
middlewareRegistry.register(metadata)
}

View File

@@ -40,7 +40,7 @@ export { validateConfig, safeValidateConfig, formatValidationError, AppConfigSch
// Testing utilities
export * from './testing'
// Middleware exports
// Middleware exports (importing these files triggers their self-registration)
export { initHelmet, type HelmetConfig } from './middlewares/security/helmet'
export { initCors, type CorsConfig } from './middlewares/security/cors'
export { initCsrf, type CsrfConfig } from './middlewares/security/csrf'
@@ -49,6 +49,9 @@ export { initJwt, signToken, type JwtConfig } from './middlewares/auth/jwt'
export { initCookieParser, type CookieParserConfig } from './middlewares/utils/cookieParser'
export { initBodyParser, type BodyParserConfig } from './middlewares/utils/bodyParser'
// Middleware registry for advanced use cases
export { middlewareRegistry, registerMiddleware, type MiddlewareMetadata } from './core/MiddlewareFactory'
// Re-export Application.create as default
export { Application as default } from './core/Application'

167
v2/middlewares/README.md Normal file
View File

@@ -0,0 +1,167 @@
# NSK Middleware System
NSK uses a **middleware registry pattern** for scalable middleware management.
## How to Add a New Middleware
### ✅ Changes Required: **1-2 Places**
1. **Create middleware file** (e.g., `v2/middlewares/security/myMiddleware.ts`)
2. **(Optional)** Export in `v2/index.ts` if needed for external use
That's it! The middleware will automatically:
- Register itself when imported
- Initialize from `armcorc.json` if configured
- Log with consistent `[NSK][MIDDLEWARE]` prefixes
- Execute in the correct order
---
## Example: Adding a New Middleware
### Step 1: Create the Middleware File
```typescript
// v2/middlewares/security/compression.ts
import compression from 'compression'
import { Application } from 'express'
import { Logger } from '../../types/Logger'
import { registerMiddleware } from '../../core/MiddlewareFactory'
export interface CompressionConfig {
enabled?: boolean
level?: number
threshold?: number
}
/**
* Initialize compression middleware
* Compresses response bodies
*/
export function initCompression(app: Application, config: CompressionConfig, logger?: Logger): void {
const options = {
level: config.level || 6,
threshold: config.threshold || 1024
}
app.use(compression(options))
logger?.debug('[NSK][COMPRESSION] Config:', options)
}
// Self-register this middleware
registerMiddleware({
name: 'compression', // Name used in armcorc.json
category: 'security', // Category for organization
order: 35, // Execution order (lower = earlier)
initializer: initCompression
})
```
### Step 2: (Optional) Export in index.ts
```typescript
// v2/index.ts
export { initCompression, type CompressionConfig } from './middlewares/security/compression'
```
### Step 3: Configure in armcorc.json
```json
{
"middlewares": {
"compression": {
"enabled": true,
"level": 6,
"threshold": 1024
}
}
}
```
**Done!** The middleware will automatically initialize when `Application.build()` is called.
---
## Middleware Execution Order
Middlewares execute based on their `order` value (lower = earlier):
- **10** - bodyParser (parse request bodies first)
- **20** - cookieParser (parse cookies)
- **30** - helmet (security headers)
- **40** - cors (cross-origin handling)
- **50** - rateLimit (rate limiting)
- **60+** - Custom middlewares
---
## Architecture Benefits
### Before (Manual Import Pattern)
Adding a middleware required changes in **4 places**:
1. Create middleware file
2. Export in index.ts
3. Add schema in ConfigSchema.ts
4. Import + call in Application.ts
### After (Registry Pattern)
Adding a middleware requires changes in **1-2 places**:
1. Create middleware file with self-registration
2. (Optional) Export in index.ts
### Key Features
-**Single source of truth**: Each middleware is self-contained
-**Automatic initialization**: From armcorc.json config
-**Consistent logging**: `[NSK][MIDDLEWARE]` prefix automatically added
-**Order management**: Defined once in middleware file
-**Enabled/disabled checks**: Handled by registry
-**Type-safe**: Full TypeScript support
---
## Registry API
```typescript
// Register a middleware
registerMiddleware({
name: 'myMiddleware',
category: 'security',
order: 35,
initializer: initMyMiddleware
})
// Get registered middleware
const middleware = middlewareRegistry.get('cors')
// Check if registered
const exists = middlewareRegistry.has('helmet')
// Get all middleware names
const names = middlewareRegistry.getRegisteredNames()
// Initialize all from config (automatically called by Application.build)
middlewareRegistry.initializeFromConfig(app, config.middlewares, logger)
```
---
## Logging
The registry automatically wraps initialization with consistent logging:
```
[NSK][BODYPARSER] Initializing...
[NSK][BODYPARSER] JSON parser enabled
[NSK][BODYPARSER] URL-encoded parser enabled
[NSK][BODYPARSER] Initialized
[NSK][HELMET] Initializing...
[NSK][HELMET] Initialized
[NSK][CORS] Initializing...
[NSK][CORS] Allowed origins: ["http://localhost:3000", ...]
[NSK][CORS] Initialized
```
Your middleware's logger calls use the same prefix, maintaining consistency.

View File

@@ -1,6 +1,7 @@
import cors, { CorsOptions } from 'cors'
import { Application, RequestHandler } from 'express'
import { Logger } from '../../types/Logger'
import { registerMiddleware } from '../../core/MiddlewareFactory'
export interface CorsConfig {
enabled?: boolean
@@ -18,13 +19,6 @@ export interface CorsConfig {
* Handles cross-origin requests with validation
*/
export function initCors(app: Application, config: CorsConfig, logger?: Logger): void {
if (config.enabled === false) {
logger?.debug('[NSK][CORS] Middleware disabled')
return
}
logger?.info('[NSK][CORS] Initializing...')
const corsOptions: CorsOptions = {
credentials: config.credentials ?? true,
maxAge: config.maxAge,
@@ -44,12 +38,12 @@ export function initCors(app: Application, config: CorsConfig, logger?: Logger):
if (config.allowedOrigins!.includes(origin)) {
callback(null, true)
} else {
logger?.warn(`Blocked CORS request from origin: ${origin}`)
logger?.warn(`[NSK][CORS] Blocked request from origin: ${origin}`)
callback(new Error(`Origin ${origin} not allowed by CORS`))
}
}
logger?.info('[NSK][CORS] Allowed origins:', { origins: config.allowedOrigins })
logger?.debug('[NSK][CORS] Allowed origins:', { origins: config.allowedOrigins })
} else {
// Default: allow all origins
corsOptions.origin = true
@@ -69,6 +63,12 @@ export function initCors(app: Application, config: CorsConfig, logger?: Logger):
}
app.use(cors(corsOptions) as RequestHandler)
logger?.info('[NSK][CORS] Initialized')
}
// Self-register this middleware
registerMiddleware({
name: 'cors',
category: 'security',
order: 40, // After helmet
initializer: initCors
})

View File

@@ -1,6 +1,7 @@
import helmet, { HelmetOptions } from 'helmet'
import { Application, RequestHandler } from 'express'
import { Logger } from '../../types/Logger'
import { registerMiddleware } from '../../core/MiddlewareFactory'
export interface HelmetConfig {
enabled?: boolean
@@ -16,13 +17,6 @@ export interface HelmetConfig {
* Sets various security headers
*/
export function initHelmet(app: Application, config: HelmetConfig, logger?: Logger): void {
if (config.enabled === false) {
logger?.debug('[NSK][HELMET] Middleware disabled')
return
}
logger?.info('[NSK][HELMET] Initializing...')
const options: HelmetOptions = config.options || {}
// Handle CSP configuration
@@ -39,6 +33,12 @@ export function initHelmet(app: Application, config: HelmetConfig, logger?: Logg
}
app.use(helmet(options) as RequestHandler)
logger?.info('[NSK][HELMET] Initialized')
}
// Self-register this middleware
registerMiddleware({
name: 'helmet',
category: 'security',
order: 30, // After parsers
initializer: initHelmet
})

View File

@@ -1,6 +1,7 @@
import rateLimit, { Options } from 'express-rate-limit'
import { Application, Request, Response } from 'express'
import { Logger } from '../../types/Logger'
import { registerMiddleware } from '../../core/MiddlewareFactory'
export interface RateLimiterConfig {
enabled?: boolean
@@ -20,13 +21,6 @@ export interface RateLimiterConfig {
* Protects against brute force and DoS attacks
*/
export function initRateLimiter(app: Application, config: RateLimiterConfig, logger?: Logger): void {
if (config.enabled === false) {
logger?.debug('[NSK][RATELIMITER] Middleware disabled')
return
}
logger?.info('[NSK][RATELIMITER] Initializing...')
const skipPaths = config.skipPaths || []
const options: Partial<Options> = {
@@ -58,8 +52,24 @@ export function initRateLimiter(app: Application, config: RateLimiterConfig, log
app.use(rateLimit(options))
logger?.info('[NSK][RATELIMITER] Initialized', {
logger?.debug('[NSK][RATELIMITER] Config:', {
windowMs: options.windowMs,
max: options.max,
})
}
// Self-register this middleware
registerMiddleware({
name: 'rateLimit',
category: 'security',
order: 50, // After CORS
initializer: initRateLimiter
})
// Also register as 'rateLimiter' alias for backward compatibility
registerMiddleware({
name: 'rateLimiter',
category: 'security',
order: 50,
initializer: initRateLimiter
})

View File

@@ -1,5 +1,6 @@
import express, { Application } from 'express'
import { Logger } from '../../types/Logger'
import { registerMiddleware } from '../../core/MiddlewareFactory'
export interface BodyParserConfig {
json?: {
@@ -17,8 +18,6 @@ export interface BodyParserConfig {
* Parses request body as JSON and URL-encoded
*/
export function initBodyParser(app: Application, config: BodyParserConfig, logger?: Logger): void {
logger?.info('[NSK][BODYPARSER] Initializing...')
// JSON parser
if (config.json?.enabled !== false) {
const jsonOptions = config.json?.options || {}
@@ -32,6 +31,12 @@ export function initBodyParser(app: Application, config: BodyParserConfig, logge
app.use(express.urlencoded(urlencodedOptions))
logger?.debug('[NSK][BODYPARSER] URL-encoded parser enabled')
}
logger?.info('[NSK][BODYPARSER] Initialized')
}
// Self-register this middleware
registerMiddleware({
name: 'bodyParser',
category: 'utils',
order: 10, // Early in chain (body parsing should be first)
initializer: initBodyParser
})

View File

@@ -1,6 +1,7 @@
import cookieParser from 'cookie-parser'
import { Application, RequestHandler } from 'express'
import { Logger } from '../../types/Logger'
import { registerMiddleware } from '../../core/MiddlewareFactory'
export interface CookieParserConfig {
enabled?: boolean
@@ -13,18 +14,19 @@ export interface CookieParserConfig {
* Parses cookies from request headers
*/
export function initCookieParser(app: Application, config: CookieParserConfig, logger?: Logger): void {
if (config.enabled === false) {
logger?.debug('[NSK][COOKIEPARSER] Middleware disabled')
return
}
logger?.info('[NSK][COOKIEPARSER] Initializing...')
if (config.secret) {
app.use(cookieParser(config.secret, config.options) as RequestHandler)
logger?.info('[NSK][COOKIEPARSER] Initialized with secret')
logger?.debug('[NSK][COOKIEPARSER] Using secret')
} else {
app.use(cookieParser(undefined, config.options) as RequestHandler)
logger?.info('[NSK][COOKIEPARSER] Initialized without secret')
logger?.debug('[NSK][COOKIEPARSER] No secret')
}
}
// Self-register this middleware
registerMiddleware({
name: 'cookieParser',
category: 'utils',
order: 20, // After body parser
initializer: initCookieParser
})