feat: auto-inject middlewares from armcorc.json
Some checks failed
armco-org/node-starter-kit/pipeline/head There was a failure building this commit

- Add bodyParser and cookieParser middleware support
- Auto-inject configured middlewares during Application.build()
- Add [NSK][MIDDLEWARE] logger prefixes for all middlewares
- Middlewares inject in correct order: bodyParser → cookieParser → helmet → cors → rateLimit
- Supports both rateLimit and rateLimiter config keys
- Zero boilerplate required in host apps - just Application.create(app).build()
- armcorc.json is single source of truth for middleware configuration
This commit is contained in:
2025-12-16 22:57:24 +05:30
parent 521b26c2f9
commit 411f63d79f
10 changed files with 148 additions and 14 deletions

View File

@@ -106,7 +106,7 @@ import pkg from "./package.json";
logger.info('✅ Build completed successfully!');
logger.info('📦 Package ready in ./dist folder');
} catch (err) {
logger.err('❌ Build failed:', err);
logger.err('❌ Build failed:', err as any);
process.exit(1);
}
})();

View File

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

View File

@@ -276,6 +276,13 @@ 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')
// Load configuration
let finalConfig: AppConfig
@@ -349,10 +356,41 @@ export class ApplicationBuilder {
application.plugins(this.pluginRegistrations)
}
// Initialize and start
// Initialize and start plugins
await application.initialize()
await application.start()
// 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)
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)
}
}
return application
}
}

View File

@@ -153,6 +153,29 @@ const RateLimiterConfigSchema = z.object({
handler: z.function().optional(),
}).passthrough()
/**
* Cookie parser middleware configuration schema
*/
const CookieParserConfigSchema = z.object({
enabled: z.boolean().optional(),
secret: z.union([z.string(), z.array(z.string())]).optional(),
options: z.record(z.unknown()).optional(),
}).passthrough()
/**
* Body parser middleware configuration schema
*/
const BodyParserConfigSchema = z.object({
json: z.object({
enabled: z.boolean().optional(),
options: z.record(z.unknown()).optional(),
}).optional(),
urlencoded: z.object({
enabled: z.boolean().optional(),
options: z.record(z.unknown()).optional(),
}).optional(),
}).passthrough()
/**
* Middleware configuration schema
*/
@@ -162,6 +185,9 @@ const MiddlewareConfigSchema = z.object({
csrf: CsrfConfigSchema.optional(),
jwt: JwtConfigSchema.optional(),
rateLimiter: RateLimiterConfigSchema.optional(),
rateLimit: RateLimiterConfigSchema.optional(), // Alias for rateLimiter
cookieParser: CookieParserConfigSchema.optional(),
bodyParser: BodyParserConfigSchema.optional(),
}).passthrough()
/**

View File

@@ -46,6 +46,8 @@ export { initCors, type CorsConfig } from './middlewares/security/cors'
export { initCsrf, type CsrfConfig } from './middlewares/security/csrf'
export { initRateLimiter, type RateLimiterConfig } from './middlewares/security/rateLimiter'
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'
// Re-export Application.create as default
export { Application as default } from './core/Application'

View File

@@ -19,11 +19,11 @@ export interface CorsConfig {
*/
export function initCors(app: Application, config: CorsConfig, logger?: Logger): void {
if (config.enabled === false) {
logger?.debug('CORS middleware disabled')
logger?.debug('[NSK][CORS] Middleware disabled')
return
}
logger?.info('Initializing CORS middleware')
logger?.info('[NSK][CORS] Initializing...')
const corsOptions: CorsOptions = {
credentials: config.credentials ?? true,
@@ -49,11 +49,11 @@ export function initCors(app: Application, config: CorsConfig, logger?: Logger):
}
}
logger?.info('CORS allowed origins:', { origins: config.allowedOrigins })
logger?.info('[NSK][CORS] Allowed origins:', { origins: config.allowedOrigins })
} else {
// Default: allow all origins
corsOptions.origin = true
logger?.warn('CORS allowing all origins (not recommended for production)')
logger?.warn('[NSK][CORS] Allowing all origins (not recommended for production)')
}
if (config.allowedMethods) {
@@ -70,5 +70,5 @@ export function initCors(app: Application, config: CorsConfig, logger?: Logger):
app.use(cors(corsOptions) as RequestHandler)
logger?.info('CORS middleware initialized')
logger?.info('[NSK][CORS] Initialized')
}

View File

@@ -17,11 +17,11 @@ export interface HelmetConfig {
*/
export function initHelmet(app: Application, config: HelmetConfig, logger?: Logger): void {
if (config.enabled === false) {
logger?.debug('Helmet middleware disabled')
logger?.debug('[NSK][HELMET] Middleware disabled')
return
}
logger?.info('Initializing Helmet middleware')
logger?.info('[NSK][HELMET] Initializing...')
const options: HelmetOptions = config.options || {}
@@ -40,5 +40,5 @@ export function initHelmet(app: Application, config: HelmetConfig, logger?: Logg
app.use(helmet(options) as RequestHandler)
logger?.info('Helmet middleware initialized')
logger?.info('[NSK][HELMET] Initialized')
}

View File

@@ -21,11 +21,11 @@ export interface RateLimiterConfig {
*/
export function initRateLimiter(app: Application, config: RateLimiterConfig, logger?: Logger): void {
if (config.enabled === false) {
logger?.debug('Rate limiter middleware disabled')
logger?.debug('[NSK][RATELIMITER] Middleware disabled')
return
}
logger?.info('Initializing rate limiter middleware')
logger?.info('[NSK][RATELIMITER] Initializing...')
const skipPaths = config.skipPaths || []
@@ -43,7 +43,7 @@ export function initRateLimiter(app: Application, config: RateLimiterConfig, log
return skipPaths.some(path => req.path.startsWith(path))
}),
handler: config.handler || ((req: Request, res: Response) => {
logger?.warn('Rate limit exceeded', {
logger?.warn('[NSK][RATELIMITER] Rate limit exceeded', {
ip: req.ip,
path: req.path,
method: req.method
@@ -58,7 +58,7 @@ export function initRateLimiter(app: Application, config: RateLimiterConfig, log
app.use(rateLimit(options))
logger?.info('Rate limiter middleware initialized', {
logger?.info('[NSK][RATELIMITER] Initialized', {
windowMs: options.windowMs,
max: options.max,
})

View File

@@ -0,0 +1,37 @@
import express, { Application } from 'express'
import { Logger } from '../../types/Logger'
export interface BodyParserConfig {
json?: {
enabled?: boolean
options?: any
}
urlencoded?: {
enabled?: boolean
options?: any
}
}
/**
* Initialize body parser middleware
* 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 || {}
app.use(express.json(jsonOptions))
logger?.debug('[NSK][BODYPARSER] JSON parser enabled')
}
// URL-encoded parser
if (config.urlencoded?.enabled !== false) {
const urlencodedOptions = config.urlencoded?.options || { extended: true }
app.use(express.urlencoded(urlencodedOptions))
logger?.debug('[NSK][BODYPARSER] URL-encoded parser enabled')
}
logger?.info('[NSK][BODYPARSER] Initialized')
}

View File

@@ -0,0 +1,30 @@
import cookieParser from 'cookie-parser'
import { Application, RequestHandler } from 'express'
import { Logger } from '../../types/Logger'
export interface CookieParserConfig {
enabled?: boolean
secret?: string | string[]
options?: cookieParser.CookieParseOptions
}
/**
* Initialize cookie parser middleware
* 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')
} else {
app.use(cookieParser(undefined, config.options) as RequestHandler)
logger?.info('[NSK][COOKIEPARSER] Initialized without secret')
}
}