Moved server config higher up in global.<app> from global.<app>.config
All checks were successful
armco-org/node-starter-kit/pipeline/head This commit looks good

This commit is contained in:
2026-02-06 01:44:06 +05:30
parent 5556d81701
commit b78a107155
10 changed files with 3323 additions and 5 deletions

View File

@@ -0,0 +1,344 @@
# Config-Driven Initialization - Breaking Change Summary
## What Changed
NSK v2 now supports **config-driven initialization** where `Application.create().build()` automatically loads and instantiates plugins from `armcorc.json` configuration.
## Problem Solved
### Before
- Config files only contained plugin **settings**
- You still had to **manually register** plugin instances in code:
```typescript
App.create(app)
.plugin(createLoggerPlugin()) // Code change required
.plugin(createDatabasePlugin()) // Code change required
.build()
```
- Changing plugins required **code changes + rebuild + redeploy**
### After
- Config file is the **single source of truth**
- `Application.create().build()` auto-loads plugins from config
- Changing plugins only requires **restart** (no rebuild)
## How It Works
### 1. Plugin Factory Registry
When you import `@armco/node-starter-kit`, all built-in plugins automatically register their factory functions:
```typescript
// Happens automatically when you import NSK
registerPluginFactory('logger', (config) => createLoggerPlugin(config))
registerPluginFactory('database', (config) => createDatabasePlugin(config))
registerPluginFactory('cache', (config) => createCachePlugin(config))
// etc...
```
### 2. Hybrid Loading Logic (Config + Manual)
```typescript
// In Application.build():
// 1. Config is PRIMARY source of truth
// 2. Manual registration OVERRIDES specific plugins
if (config has plugins) {
// Auto-load plugins from config
for (pluginName, pluginConfig in config.plugins) {
if (pluginName NOT manually registered && enabled !== false) {
const factory = pluginRegistry.get(pluginName)
const plugin = factory(pluginConfig)
application.plugin(plugin)
}
}
// Add manually registered plugins (these override config)
for (manualPlugin in manualPlugins) {
application.plugin(manualPlugin)
}
}
```
**Key Behavior:**
- Config defines **what** plugins are enabled
- Manual registration **overrides** specific plugin instances
- Manually registered plugins should still be declared in config (for consistency)
## Usage
### Approach 1: Pure Config-Driven (Recommended)
**server.ts:**
```typescript
import express from 'express'
import { Application } from '@armco/node-starter-kit'
const app = express()
const nsk = await Application.create(app).build() // That's it!
app.listen(3000)
```
**armcorc.json:**
```json
{
"appName": "auth-core",
"plugins": {
"logger": { "enabled": true, "level": "info" },
"database": { "enabled": true, "uri": "mongodb://localhost/authdb" },
"cache": { "enabled": true, "adapter": "redis" }
}
}
```
**To change config:**
1. Edit `armcorc.json`
2. Restart the app
3. Done! (no rebuild needed)
### Approach 2: Programmatic Config (Cloud Config)
```typescript
import { Application } from '@armco/node-starter-kit'
// Load from config server, K8s ConfigMap, env vars, etc.
const config = {
appName: 'auth-core',
plugins: {
logger: {
enabled: true,
level: process.env.LOG_LEVEL || 'info'
},
database: {
enabled: true,
uri: process.env.MONGODB_URI
},
},
}
const nsk = await Application.create(app)
.withConfig(config)
.build()
```
### Approach 3: Hybrid (RECOMMENDED for most cases)
Config is primary source of truth, manual registration for overrides:
**armcorc.json:**
```json
{
"appName": "auth-core",
"plugins": {
"logger": { "enabled": true, "level": "info" },
"database": { "enabled": true, "uri": "mongodb://..." },
"cache": { "enabled": true },
"socket": { "enabled": true }
}
}
```
**server.ts:**
```typescript
import http from 'http'
import { Application, createSocketPlugin } from '@armco/node-starter-kit'
const app = express()
const server = http.createServer(app)
const nsk = await Application.create(app)
// Config auto-loads: logger, database, cache
// Manual registration for socket (requires server instance)
.plugin(createSocketPlugin(server))
.build()
server.listen(3000)
```
**Benefits:**
- Config declares all plugins (single source of truth)
- Code only handles runtime requirements (e.g., HttpServer for socket)
- Easy restart-based reconfiguration for most plugins
### Approach 4: Pure Manual (Still Supported)
```typescript
import { Application, createLoggerPlugin } from '@armco/node-starter-kit'
// Explicit plugin registration (old way, still works)
const nsk = await Application.create(app)
.plugin(createLoggerPlugin({ level: 'info' }))
.plugin(createDatabasePlugin({ uri: 'mongodb://...' }))
.build()
```
## Supported Plugins
These plugins can be auto-loaded from config:
| Plugin Name | Config Key | Notes |
|-------------|------------|-------|
| Logger | `logger` | ✅ Fully supported |
| Database | `database` | ✅ Fully supported |
| Cache | `cache` | ✅ Fully supported |
| Scheduler | `scheduler` | ✅ Fully supported |
| Telemetry | `telemetry` or `opentelemetry` | ✅ Fully supported |
| Socket.IO | `socket` | ⚠️ **Requires manual registration** (needs `HttpServer` instance) |
### Socket.IO Special Case
Socket.IO plugin requires an `HttpServer` instance, which cannot be provided via JSON config:
```typescript
import http from 'http'
import { Application, createSocketPlugin } from '@armco/node-starter-kit'
const app = express()
const server = http.createServer(app)
const nsk = await Application.create(app)
// Auto-load other plugins from config
.withConfig('./armcorc.json')
// Manually add socket (requires server instance)
.plugin(createSocketPlugin(server, { cors: { origin: '*' } }))
.build()
server.listen(3000)
```
## Configuration Examples
### Minimal Config
```json
{
"appName": "my-service",
"plugins": {
"logger": { "enabled": true }
}
}
```
### Production Config
```json
{
"appName": "auth-core",
"env": "production",
"plugins": {
"logger": {
"enabled": true,
"level": "info",
"format": "json"
},
"database": {
"enabled": true,
"adapter": "mongoose",
"uri": "mongodb://mongo:27017/authdb",
"options": {
"maxPoolSize": 10
}
},
"cache": {
"enabled": true,
"adapter": "redis",
"uri": "redis://redis:6379",
"ttl": 3600,
"keyPrefix": "auth:"
},
"telemetry": {
"enabled": true,
"serviceName": "auth-core",
"serviceVersion": "1.0.0",
"otlpEndpoint": "http://tempo:4318",
"exporters": ["otlp"]
}
}
}
```
### Disable a Plugin
```json
{
"appName": "my-service",
"plugins": {
"logger": { "enabled": true },
"cache": { "enabled": false } // Disabled
}
}
```
## Migration Path
### If You're Happy with Current Approach
**No changes needed!** Manual plugin registration still works:
```typescript
const nsk = await Application.create(app)
.plugin(createLoggerPlugin())
.build()
```
### If You Want Config-Driven
1. **Create `armcorc.json`** with your plugin configs
2. **Remove manual `.plugin()` calls**
3. **Simplify to** `Application.create(app).build()`
## Benefits
1. **Restart-based reconfiguration**: No rebuild or redeploy needed
2. **GitOps friendly**: Config changes tracked in version control
3. **Cloud config integration**: Load from K8s ConfigMaps, AWS Parameter Store, etc.
4. **Environment-specific**: Easy to swap configs per environment
5. **Cleaner code**: Less boilerplate in application code
## Runtime Reconfiguration Workflow
```bash
# 1. Edit config
vim armcorc.json
# 2. Commit (optional, for GitOps)
git commit -am "Enable cache plugin"
# 3. Restart app (PM2, K8s, systemd, etc.)
pm2 reload my-service
# or
kubectl rollout restart deployment/my-service
# 4. New config is active (no rebuild!)
```
## Implementation Details
### Files Changed/Added
**New:**
- `v2/core/PluginFactory.ts` - Plugin factory registry
- `v2/examples/config-driven-initialization.ts` - Usage examples
- `v2/examples/armco.config.full.json` - Full config reference
- `v2/CONFIG_DRIVEN_INITIALIZATION.md` - Comprehensive docs
**Modified:**
- `v2/core/Application.ts` - Auto-load plugins from config
- `v2/index.ts` - Export PluginFactory
- All plugin `index.ts` files - Register factories on import
### Type Safety
Config is still validated using Zod schemas, ensuring type safety at runtime.
## Questions?
- See `/v2/examples/config-driven-initialization.ts` for complete examples
- See `/v2/CONFIG_DRIVEN_INITIALIZATION.md` for detailed documentation
- See `/v2/examples/armco.config.full.json` for full config reference
## Summary
**Backward compatible** - Old approach still works
**Opt-in** - Use config-driven only if you want
**Restart-based reconfiguration** - No rebuild needed
**Cloud-native** - Integrates with config management systems
**Type-safe** - Still validated with Zod schemas

View File

@@ -0,0 +1,372 @@
# Config-Driven Initialization
NSK v2 now supports **config-driven initialization**, enabling restart-based reconfiguration without code changes or rebuilds. This document explains how it works and how to use it.
## Overview
### Previous Approach (Still Supported)
```typescript
import { Application, createLoggerPlugin, createDatabasePlugin } from '@armco/node-starter-kit'
const nsk = await Application.create(app)
.plugin(createLoggerPlugin({ level: 'info' }))
.plugin(createDatabasePlugin({ uri: process.env.MONGODB_URI }))
.build()
```
### New Config-Driven Approach
```typescript
import { Application } from '@armco/node-starter-kit'
// Just this - plugins auto-loaded from armcorc.json
const nsk = await Application.create(app).build()
```
**armcorc.json:**
```json
{
"appName": "my-service",
"plugins": {
"logger": { "enabled": true, "level": "info" },
"database": { "enabled": true, "uri": "mongodb://localhost/mydb" }
}
}
```
## Benefits
1. **Restart-based reconfiguration**: Change config → restart app → changes applied (no rebuild)
2. **Single source of truth**: All configuration in one place (JSON/YAML)
3. **Cloud config integration**: Load from config server, env vars, K8s ConfigMaps, etc.
4. **Environment-specific configs**: Easy to swap configs per environment
5. **GitOps friendly**: Config changes tracked in version control
## How It Works
### Plugin Factory Registry
NSK maintains a global registry that maps plugin names to factory functions:
```typescript
// Internally, when you import @armco/node-starter-kit:
registerPluginFactory('logger', (config) => createLoggerPlugin(config))
registerPluginFactory('database', (config) => createDatabasePlugin(config))
registerPluginFactory('cache', (config) => createCachePlugin(config))
// ... etc
```
### Auto-Loading Process
When `Application.create(app).build()` is called **without** explicit plugins:
1. NSK searches for `armcorc.json` (or `.armcorc`, `armco.config.js`, etc.)
2. Reads the `plugins` section
3. For each enabled plugin, looks up its factory in the registry
4. Instantiates the plugin with its config
5. Registers and starts all plugins
## Usage Patterns
### Pattern 1: Pure Config-Driven (Recommended)
**server.ts:**
```typescript
import express from 'express'
import { Application } from '@armco/node-starter-kit'
const app = express()
// All plugins loaded from armcorc.json
const nsk = await Application.create(app).build()
app.listen(3000)
```
**armcorc.json:**
```json
{
"appName": "auth-core",
"plugins": {
"logger": {
"enabled": true,
"level": "info",
"format": "json"
},
"database": {
"enabled": true,
"adapter": "mongoose",
"uri": "mongodb://localhost:27017/authdb"
},
"cache": {
"enabled": true,
"adapter": "redis",
"uri": "redis://localhost:6379"
}
}
}
```
### Pattern 2: Cloud Config Integration
```typescript
import { Application } from '@armco/node-starter-kit'
// Load from cloud config store
const config = await fetchConfigFromCloudStore()
const nsk = await Application.create(app)
.withConfig(config)
.build()
```
### Pattern 3: Environment Variables
```typescript
import { Application } from '@armco/node-starter-kit'
const config = {
appName: 'auth-core',
plugins: {
logger: {
enabled: true,
level: process.env.LOG_LEVEL || 'info',
},
database: {
enabled: true,
uri: process.env.MONGODB_URI,
},
},
}
const nsk = await Application.create(app)
.withConfig(config)
.build()
```
### Pattern 4: Mixed (Config + Manual)
Use config for standard plugins, manual registration for special cases:
```typescript
import { Application } from '@armco/node-starter-kit'
import http from 'http'
const app = express()
const server = http.createServer(app)
const nsk = await Application.create(app)
// Auto-load plugins from config
.withConfig('./armcorc.json')
// Manually add socket (requires server instance)
.plugin(createSocketPlugin(server))
.build()
```
## Configuration Reference
### Config File Locations
NSK searches for configuration in this order:
1. `armco.config.js` (TypeScript: `armco.config.ts`)
2. `armco.config.json`
3. `.armcorc`
4. `.armcorc.json`
5. `.armcorc.js`
6. `armcorc.json`
7. `package.json` (under `"armco"` key)
### Plugin Configuration Schema
Each plugin in `plugins` object follows this structure:
```json
{
"plugins": {
"<plugin-name>": {
"enabled": true, // Set to false to disable
"priority": 10, // Load order (optional)
"<plugin-specific-config>": "..."
}
}
}
```
### Supported Plugin Keys
| Key | Plugin | Config Type |
|-----|--------|-------------|
| `logger` | Logger | `LoggerConfig` |
| `database` | Database | `DatabaseConfig` |
| `cache` | Cache | `CacheConfig` |
| `scheduler` | Scheduler | `SchedulerConfig` |
| `telemetry` or `opentelemetry` | OpenTelemetry | `OpenTelemetryConfig` |
**Note**: `socket` plugin cannot be auto-loaded (requires `HttpServer` instance).
## Migration Guide
### If You're Using Manual Plugin Registration
**Before:**
```typescript
const nsk = await Application.create(app)
.plugin(createLoggerPlugin({ level: 'info' }))
.plugin(createDatabasePlugin({ uri: 'mongodb://...' }))
.build()
```
**After (Option 1 - Config File):**
Create `armcorc.json`:
```json
{
"appName": "my-service",
"plugins": {
"logger": { "enabled": true, "level": "info" },
"database": { "enabled": true, "uri": "mongodb://..." }
}
}
```
Update code:
```typescript
const nsk = await Application.create(app).build()
```
**After (Option 2 - Inline Config):**
```typescript
const nsk = await Application.create(app)
.withConfig({
appName: 'my-service',
plugins: {
logger: { enabled: true, level: 'info' },
database: { enabled: true, uri: 'mongodb://...' },
},
})
.build()
```
## Runtime Reconfiguration
### Pattern: Graceful Reload
1. **Update config file** (manually, API, or config management tool)
2. **Send SIGTERM** to the process
3. **Process manager restarts** the app (PM2, K8s, systemd, etc.)
4. **NSK loads new config** on startup
```bash
# Example with PM2
vim armcorc.json # Edit config
pm2 reload my-service # Graceful reload
```
### Pattern: Hot Reload (Advanced)
For hot reload without restart, you'll need custom logic:
```typescript
import { watch } from 'fs'
import { Application } from '@armco/node-starter-kit'
let nsk: Application
async function loadApp() {
if (nsk) {
await nsk.shutdown()
}
nsk = await Application.create(app).build()
}
// Initial load
await loadApp()
// Watch for config changes
watch('./armcorc.json', async () => {
console.log('Config changed, reloading...')
await loadApp()
})
```
## Custom Plugins
To make your custom plugin auto-loadable:
```typescript
import { registerPluginFactory, type PluginFactory } from '@armco/node-starter-kit'
export class MyCustomPlugin extends BasePlugin<MyConfig> {
// ... implementation
}
export function createMyPlugin(config: MyConfig) {
return new MyCustomPlugin(config)
}
// Register factory
registerPluginFactory('mycustom', (config) =>
createMyPlugin(config as unknown as MyConfig)
)
```
Then use in config:
```json
{
"plugins": {
"mycustom": {
"enabled": true,
"myOption": "value"
}
}
}
```
## Best Practices
1. **Use config files for production**: More maintainable than inline configs
2. **Environment-specific configs**: Use separate files (`armcorc.prod.json`, `armcorc.dev.json`)
3. **Sensitive data**: Use env vars, not config files:
```typescript
const config = await ConfigLoader.load()
config.plugins.database.uri = process.env.MONGODB_URI
```
4. **Validate configs**: NSK uses Zod for validation, but add your own checks
5. **Version control**: Commit configs (except secrets) for GitOps workflows
6. **Documentation**: Document your config schema for your team
## Troubleshooting
### "Plugin factory not found"
**Cause**: Plugin not imported, so factory wasn't registered.
**Solution**: Import the main package:
```typescript
import '@armco/node-starter-kit' // Triggers factory registration
```
Or import specific plugin:
```typescript
import '@armco/node-starter-kit/plugins/logger'
```
### Config not loading
**Cause**: Config file not found or invalid.
**Solution**: Check file name and location. Enable debug:
```typescript
const loader = new ConfigLoader()
const config = loader.load()
console.log('Loaded config:', config)
```
### Plugin not initializing
**Cause**: Missing required config fields or `enabled: false`.
**Solution**: Check plugin config requirements in type definitions.
## Examples
See `/v2/examples/config-driven-initialization.ts` for complete examples.

465
docs/ERROR_HANDLING.md Normal file
View File

@@ -0,0 +1,465 @@
# Error Handling Guide
## Actionable Error Messages
Node Starter Kit v2 provides detailed, actionable error messages to help you quickly resolve configuration and runtime issues.
## Common Errors and Solutions
### Configuration Errors
#### Error: "appName is required and cannot be empty"
**Cause**: Missing or empty `appName` in configuration
**Solution**:
```typescript
// armco.config.ts
export default {
appName: 'my-app', // ✅ Add this
// ... rest of config
}
```
---
#### Error: "Database URI is required"
**Cause**: Database plugin enabled but no URI provided
**Solution**:
```typescript
export default {
plugins: {
database: {
uri: process.env.MONGO_URI || 'mongodb://localhost:27017/mydb' // ✅ Add URI
}
}
}
```
**Check**:
```bash
# Verify environment variable is set
echo $MONGO_URI
# Set it if missing
export MONGO_URI="mongodb://localhost:27017/mydb"
```
---
#### Error: "Either secret, secretProvider, or publicKey must be provided"
**Cause**: JWT middleware configured without authentication credentials
**Solution**:
```typescript
export default {
middlewares: {
jwt: {
// Option 1: Direct secret (not recommended for production)
secret: process.env.JWT_SECRET,
// Option 2: Secret provider (recommended)
secretProvider: () => process.env.JWT_SECRET,
// Option 3: Public key for RS256/ES256
publicKey: fs.readFileSync('public.pem', 'utf8'),
algorithms: ['RS256']
}
}
}
```
**Generate secret**:
```bash
# Generate a secure random secret
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Add to .env
echo "JWT_SECRET=your_generated_secret" >> .env
```
---
#### Error: 'Algorithm "none" is not allowed for security reasons'
**Cause**: JWT configuration includes the insecure "none" algorithm
**Solution**:
```typescript
export default {
middlewares: {
jwt: {
// Don't do this
algorithms: ['HS256', 'none'],
// Use secure algorithms only
algorithms: ['RS256', 'ES256', 'HS256']
}
}
}
```
---
#### Error: "No configuration found"
**Cause**: Config file not found or in wrong location
**Solution**:
1. Create `armco.config.ts` in project root:
```typescript
export default {
appName: 'my-app',
// ... config
}
```
2. Or specify path explicitly:
```typescript
const nsk = await Application.create(app)
.withConfig('./path/to/config.ts')
.build()
```
**Expected file locations** (searched in order):
- `armco.config.js`
- `armco.config.ts`
- `armco.config.json`
- `.armcorc`
- `.armcorc.json`
- `.armcorc.js`
- `armcorc.json`
---
#### Error: "TypeScript configuration files require ts-node or tsx to be installed"
**Cause**: Using `.ts` config file without TypeScript loader
**Solution**:
```bash
# Install tsx (recommended)
npm install --save-dev tsx
# Or install ts-node
npm install --save-dev ts-node @types/node
```
---
### Runtime Errors
#### Error: "Service 'logger' not found in container"
**Cause**: Trying to resolve a service that wasn't registered
**Solution**:
```typescript
// Make sure plugin is registered
const nsk = await Application.create(app)
.plugin(createLoggerPlugin()) // Register logger plugin
.build()
// Then resolve
const logger = nsk.getContainer().resolve('logger')
```
**Check what's registered**:
```typescript
const registered = nsk.getContainer().getServiceNames()
console.log('Registered services:', registered)
```
---
#### Error: "Plugin 'my-plugin' depends on 'logger' which is not registered"
**Cause**: Plugin dependencies not met
**Solution**:
```typescript
// Register dependencies first
const nsk = await Application.create(app)
.plugin(createLoggerPlugin()) // Register logger first
.plugin(createDatabasePlugin()) // Database depends on logger
.plugin(new MyCustomPlugin()) // Custom plugin depends on both
.build()
```
**Plugin dependency order is automatic** - just register all required plugins.
---
#### Error: "Circular dependency detected involving 'pluginA'"
**Cause**: Two or more plugins depend on each other
**Solution**:
```typescript
// Don't do this
class PluginA extends BasePlugin {
dependencies = ['pluginB']
}
class PluginB extends BasePlugin {
dependencies = ['pluginA']
}
// Refactor to remove circular dependency
// Move shared functionality to a separate plugin
class SharedPlugin extends BasePlugin {
name = 'shared'
}
class PluginA extends BasePlugin {
dependencies = ['shared']
}
class PluginB extends BasePlugin {
dependencies = ['shared']
}
```
---
#### Error: "Database circuit breaker opened"
**Cause**: Database connection failed multiple times
**What it means**: Circuit breaker is protecting your application from cascading failures
**Solution**:
1. Check database is running:
```bash
# MongoDB
mongosh --eval "db.adminCommand('ping')"
# Docker
docker ps | grep mongo
```
2. Check connection string:
```bash
echo $MONGO_URI
# Should look like: mongodb://localhost:27017/mydb
```
3. Circuit breaker will auto-close after timeout:
```typescript
database: {
circuitBreaker: {
enabled: true,
threshold: 5, // Opens after 5 failures
timeout: 60000, // Tries again after 60 seconds
}
}
```
4. Monitor circuit breaker:
```typescript
database: {
circuitBreaker: {
onOpen: () => {
logger.error('Circuit opened - DB unreachable')
// Send alert, update health check, etc.
},
onClose: () => {
logger.info('Circuit closed - DB recovered')
}
}
}
```
---
#### Error: "CSRF token missing"
**Cause**: Request doesn't include CSRF token
**Solution** (Frontend):
```typescript
// 1. Fetch CSRF token on page load
const response = await fetch('/csrf-token')
const { csrfToken } = await response.json()
// 2. Include token in requests
await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken // Add this header
},
body: JSON.stringify(data)
})
```
**Solution** (Backend - exclude path):
```typescript
csrf: {
excludePaths: ['/api/webhooks', '/auth/login'] // Exclude public endpoints
}
```
---
#### Error: "Unauthorized Access" (JWT)
**Cause**: JWT token missing, invalid, or expired
**Solution**:
1. **Token missing**: Include token in request
```typescript
await fetch('/api/protected', {
headers: {
'Authorization': `Bearer ${token}` // Add bearer token
}
})
```
2. **Token expired**: Refresh the token
```typescript
// Implement token refresh logic
if (error.status === 401) {
const newToken = await refreshToken()
// Retry request with new token
}
```
3. **Token invalid**: Re-authenticate
```typescript
// Redirect to login
window.location.href = '/login'
```
4. **Public paths**: Exclude from authentication
```typescript
jwt: {
publicPaths: ['/health', '/auth/login', '/auth/register']
}
```
---
### Health Check Errors
#### Health status: "unhealthy"
**What to check**:
1. Visit `/health` endpoint to see which check failed
2. Check individual components:
- Database: `/health/ready`
- Memory: Check memory usage
- Custom checks: Review custom check logic
**Example response**:
```json
{
"status": "unhealthy",
"checks": {
"database": {
"status": "unhealthy",
"error": "Connection timeout"
},
"memory": {
"status": "healthy",
"heapUsedPercent": 45
}
}
}
```
**Solution**: Fix the failing component (usually database connection)
---
## Debugging Tips
### Enable Debug Logging
```typescript
// armco.config.ts
export default {
plugins: {
logger: {
level: 'debug', // Enable debug logs
format: 'pretty'
}
}
}
```
### Check Container Contents
```typescript
// See what services are registered
console.log('Services:', nsk.getContainer().getServiceNames())
// Check if specific service exists
if (nsk.getContainer().has('database')) {
console.log('Database is registered')
}
```
### Validate Config Before Starting
```typescript
import { safeValidateConfig } from '@armco/node-starter-kit/v2'
const result = safeValidateConfig(rawConfig)
if (!result.success) {
console.error('Config errors:', result.error.format())
process.exit(1)
}
```
### Monitor Plugin Lifecycle
```typescript
class MyPlugin extends BasePlugin {
async install(ctx) {
console.log('✓ MyPlugin: install')
await super.install(ctx)
}
async start() {
console.log('✓ MyPlugin: start')
}
async stop() {
console.log('✓ MyPlugin: stop')
}
}
```
---
## Getting Help
If you're stuck:
1. **Check logs**: Look for detailed error messages with stack traces
2. **Validate config**: Use `safeValidateConfig()` to check configuration
3. **Check examples**: See [examples directory](./examples/)
4. **Read docs**: Check [README](./README.md) and [Migration Guide](./MIGRATION_GUIDE.md)
5. **Open issue**: [GitHub Issues](https://github.com/ReStruct-Corporate-Advantage/node-starter-kit/issues)
---
## Error Message Format
All NSK errors follow this format:
```
[Context] Error description
Suggested actions:
1. Action one
2. Action two
Examples:
- Example command or code snippet
Related: link to documentation
```
This helps you quickly understand and fix the issue!

370
docs/GLOBAL_LOGGER.md Normal file
View File

@@ -0,0 +1,370 @@
# Global Logger Access
NSK v2 provides first-class global access to the logger, matching the legacy NSK v1 behavior. Once NSK is initialized with the logger plugin, the logger is automatically available throughout your application **without requiring imports**.
## How It Works
### 1. Automatic Global Injection
When the logger plugin is installed, NSK automatically makes it available on the global object:
```typescript
// In your server.ts
import { Application } from '@armco/node-starter-kit'
const nsk = await Application.create(app)
.withConfig({
appName: 'my-service',
plugins: {
logger: { enabled: true, level: 'info' }
}
})
.build()
// Logger is now globally available!
logger.info('Application started')
```
### 2. TypeScript Type Safety
NSK exports a global namespace augmentation (`globals.d.ts`) that provides full TypeScript type safety for the global logger:
```typescript
// globals.d.ts (automatically included when you install NSK)
declare global {
var logger: Logger
}
```
**No configuration needed in your project!** Just install NSK and TypeScript will automatically recognize the `logger` global.
## Usage
### In Route Handlers
```typescript
app.get('/api/users/:id', (req, res) => {
// No import needed!
logger.info('User request received', { userId: req.params.id })
try {
const user = getUserById(req.params.id)
logger.info('User found', { user })
res.json(user)
} catch (error) {
logger.error('Failed to fetch user', { error })
res.status(500).json({ error: 'Internal server error' })
}
})
```
### In Service Classes
```typescript
class UserService {
async createUser(data: any) {
// Logger is globally available
logger.info('Creating user', { email: data.email })
try {
const user = await db.users.create(data)
logger.info('User created', { userId: user.id })
return user
} catch (error) {
logger.error('Failed to create user', { error })
throw error
}
}
}
```
### In Middleware
```typescript
function requestLogger(req, res, next) {
const start = Date.now()
logger.info('Incoming request', {
method: req.method,
path: req.path,
ip: req.ip,
})
res.on('finish', () => {
const duration = Date.now() - start
logger.info('Request completed', {
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: `${duration}ms`,
})
})
next()
}
```
### In Error Handlers
```typescript
function errorHandler(err, req, res, next) {
logger.error('Unhandled error', {
error: err.message,
stack: err.stack,
path: req.path,
})
res.status(500).json({ error: 'Internal server error' })
}
```
### In Background Jobs
```typescript
async function processQueue() {
logger.info('Processing queue')
while (true) {
const job = await queue.getNext()
if (!job) {
logger.debug('No jobs in queue')
await sleep(1000)
continue
}
try {
logger.info('Processing job', { jobId: job.id })
await processJob(job)
logger.info('Job completed', { jobId: job.id })
} catch (error) {
logger.error('Job failed', { jobId: job.id, error })
}
}
}
```
## Benefits
### 1. No Import Boilerplate
**Before (with imports):**
```typescript
import { logger } from './logger' // Repeated in every file
import { logger } from '../utils/logger' // Different paths
import { getLogger } from '@armco/node-starter-kit' // Different ways
```
**After (global):**
```typescript
// No imports needed - logger is just available!
logger.info('Request received')
```
### 2. Consistency
Every file uses the same logger instance automatically:
- No confusion about which logger to import
- No risk of multiple logger instances
- Configuration changes apply everywhere instantly
### 3. Cleaner Code
```typescript
// ❌ With imports (verbose)
import express from 'express'
import { logger } from '../utils/logger'
function handler(req, res) {
logger.info('Processing request')
// ...
}
// ✅ With global (clean)
import express from 'express'
function handler(req, res) {
logger.info('Processing request')
// ...
}
```
### 4. Migration Compatibility
Matches legacy NSK v1 behavior - existing code works without changes!
## TypeScript Configuration
### No Configuration Needed! ✅
When you install `@armco/node-starter-kit`, TypeScript automatically recognizes the global logger through the exported `globals.d.ts` file.
### How It Works
1. NSK exports `globals.d.ts` in its package
2. TypeScript automatically picks it up via the triple-slash reference in `index.ts`:
```typescript
/// <reference path="./globals.d.ts" />
```
3. Your project gets full type safety without any configuration!
### If You Need Manual Configuration (Rare)
In very rare cases, if TypeScript doesn't pick up the types automatically, you can add this to your `tsconfig.json`:
```json
{
"compilerOptions": {
"types": ["@armco/node-starter-kit"]
}
}
```
But this is **usually not necessary** - the types are included automatically.
## Best Practices
### 1. Initialize NSK Early
Ensure NSK is initialized before using the logger:
```typescript
// server.ts
import { Application } from '@armco/node-starter-kit'
async function main() {
// Initialize NSK first
const nsk = await Application.create(app)
.withConfig('./armcorc.json')
.build()
// Now logger is available
logger.info('Application initialized')
// Import other modules (they can now use logger)
const { setupRoutes } = await import('./routes')
setupRoutes(app)
}
main()
```
### 2. Structured Logging
Use structured logging with context objects:
```typescript
// ✅ Good: Structured with context
logger.info('User login', {
userId: user.id,
email: user.email,
ip: req.ip,
timestamp: new Date().toISOString(),
})
// ❌ Less good: String concatenation
logger.info(`User ${user.id} logged in from ${req.ip}`)
```
### 3. Appropriate Log Levels
Use the right log level for each situation:
```typescript
logger.debug('Detailed debugging info') // Development only
logger.info('Normal operations') // General info
logger.warn('Something unusual') // Potential issues
logger.error('Something failed', { error }) // Errors
```
### 4. Error Logging
Always include error objects and context:
```typescript
try {
await riskyOperation()
} catch (error) {
logger.error('Operation failed', {
error, // Full error object
operation: 'riskyOperation',
userId: user.id,
context: { /* additional info */ }
})
}
```
## Comparison: Legacy vs V2
### Legacy NSK v1
**Required manual namespace declaration in host project:**
```typescript
// In your project's types/global.d.ts (had to create this)
declare global {
namespace NodeJS {
interface Global {
logger: any
}
}
var logger: any
}
```
### NSK v2
**No manual configuration needed!**
Just install NSK and the types are automatically available. The namespace augmentation is exported from NSK itself.
## Troubleshooting
### "Cannot find name 'logger'"
**Cause**: NSK not initialized yet or logger plugin not enabled.
**Solution**:
1. Ensure logger plugin is enabled in `armcorc.json`
2. Initialize NSK before using the logger
3. Check that NSK initialization completed successfully
### TypeScript error: "Property 'logger' does not exist"
**Cause**: TypeScript not picking up the global types (very rare).
**Solution**:
1. Restart your TypeScript language server
2. Check that `@armco/node-starter-kit` is installed
3. If still not working, add to `tsconfig.json`:
```json
{
"compilerOptions": {
"types": ["@armco/node-starter-kit"]
}
}
```
### Logger not available in some files
**Cause**: File is imported/executed before NSK initialization.
**Solution**: Ensure NSK is initialized before importing other application modules:
```typescript
// ✅ Good
async function main() {
await initNSK() // Initialize first
const routes = await import('./routes') // Then import
}
// ❌ Bad
import { routes } from './routes' // Imported before NSK init
async function main() {
await initNSK()
}
```
## Examples
See `/v2/examples/global-logger-usage.ts` for complete working examples.

482
docs/MIGRATION_GUIDE.md Normal file
View File

@@ -0,0 +1,482 @@
# Migration Guide: v1 → v2
## Overview
Node Starter Kit v2 is a complete architectural overhaul focused on modularity, security, and developer experience. This guide will help you migrate from v1.x to v2.0.
## Breaking Changes
### 1. Global Namespace Removed
**v1:**
```typescript
import initNodeStarterKit from '@armco/node-starter-kit'
initNodeStarterKit(app)
global.logger.info('Hello')
global.db.collection('users').find()
```
**v2:**
```typescript
import { Application, createLoggerPlugin } from '@armco/node-starter-kit/v2'
const nsk = await Application.create(app)
.plugin(createLoggerPlugin())
.build()
const logger = nsk.getContainer().resolve('logger')
const db = nsk.getContainer().resolve('database')
logger.info('Hello')
```
### 2. Configuration File Format
**v1:** `armcorc.json`
```json
{
"APP_NAME": "my-app",
"modules": {
"LOG": true,
"DB": {
"development": {
"connection_string": "mongodb://localhost/mydb"
}
}
}
}
```
**v2:** `armco.config.ts` (TypeScript with validation)
```typescript
export default {
appName: 'my-app',
plugins: {
logger: { level: 'info' },
database: {
uri: process.env.MONGO_URI || 'mongodb://localhost/mydb'
}
}
}
```
### 3. Initialization API
**v1:**
```typescript
import initNodeStarterKit from '@armco/node-starter-kit'
initNodeStarterKit(app, {
server: httpServer,
supersedeModules: {
LOG: true,
DB: true
},
supersedeMiddlewares: {
helmet: true,
cors: true
}
})
```
**v2:**
```typescript
import { Application } from '@armco/node-starter-kit/v2'
import { initHelmet, initCors } from '@armco/node-starter-kit/v2'
const nsk = await Application.create(app)
.withConfig('./armco.config.ts')
.build()
const logger = nsk.getContainer().resolve('logger')
initHelmet(app, { /* config */ }, logger)
initCors(app, { /* config */ }, logger)
```
### 4. CSRF Protection
**v1:** Used deprecated `csurf` package
**v2:** Modern double-submit cookie pattern
```typescript
import { initCsrf } from '@armco/node-starter-kit/v2'
initCsrf(app, {
secret: process.env.CSRF_SECRET,
cookieOptions: {
httpOnly: true,
secure: true,
sameSite: 'strict'
}
}, logger)
// Client must:
// 1. GET /csrf-token
// 2. Include token in X-CSRF-Token header
```
### 5. JWT Authentication
**v1:**
```typescript
// Config-based with hardcoded secrets
middlewares: {
authentication: {
arOptions: {
secretKey: "my-secret",
algorithm: "HS256"
}
}
}
```
**v2:**
```typescript
import { initJwt } from '@armco/node-starter-kit/v2'
initJwt(app, {
secretProvider: () => process.env.JWT_SECRET, // Enforces env vars
algorithms: ['RS256', 'ES256'], // Secure algorithms only
issuer: 'my-app',
publicPaths: ['/health', '/auth/login']
}, logger)
```
## Migration Steps
### Step 1: Update Dependencies
```bash
npm install @armco/node-starter-kit@2.0.0
npm install cosmiconfig tsx
npm uninstall csurf # Deprecated package
```
### Step 2: Create New Config File
Create `armco.config.ts`:
```typescript
export default {
appName: 'your-app-name',
plugins: {
logger: {
level: process.env.LOG_LEVEL || 'info',
format: process.env.NODE_ENV === 'production' ? 'json' : 'pretty',
transports: [
{ type: 'console' },
{ type: 'file', filename: 'app.log' }
]
},
database: {
uri: process.env.MONGO_URI,
circuitBreaker: {
enabled: true,
threshold: 5,
timeout: 60000
}
}
},
health: {
enabled: true,
endpoint: '/health'
}
})
```
### Step 3: Update Application Entry Point
**Before:**
```typescript
import express from 'express'
import initNodeStarterKit from '@armco/node-starter-kit'
const app = express()
initNodeStarterKit(app, {
supersedeModules: {
LOG: true,
DB: true
}
})
app.listen(3000, () => {
global.logger.info('Server started')
})
```
**After:**
```typescript
import express from 'express'
import { Application } from '@armco/node-starter-kit/v2'
import { createLoggerPlugin, createDatabasePlugin } from '@armco/node-starter-kit/v2'
async function main() {
const app = express()
const nsk = await Application.create(app)
.withConfig('./armco.config.ts')
.plugin(createLoggerPlugin())
.plugin(createDatabasePlugin({
uri: process.env.MONGO_URI
}))
.build()
const logger = nsk.getContainer().resolve('logger')
app.listen(3000, () => {
logger.info('Server started')
})
// Graceful shutdown is automatic
}
main().catch(console.error)
```
### Step 4: Update Middleware Initialization
**Before:**
```typescript
// Middlewares initialized automatically from config
```
**After:**
```typescript
import {
initHelmet,
initCors,
initJwt,
initRateLimiter
} from '@armco/node-starter-kit/v2'
const logger = nsk.getContainer().resolve('logger')
initHelmet(app, {
contentSecurityPolicy: { /* config */ }
}, logger)
initCors(app, {
allowedOrigins: ['http://localhost:3000'],
credentials: true
}, logger)
initJwt(app, {
secretProvider: () => process.env.JWT_SECRET,
algorithms: ['RS256'],
publicPaths: ['/health', '/auth/login']
}, logger)
initRateLimiter(app, {
windowMs: 15 * 60 * 1000,
max: 100
}, logger)
```
### Step 5: Update Service Access
**Before:**
```typescript
// Anywhere in your code
global.logger.info('Message')
global.db.collection('users').find()
```
**After:**
```typescript
// Pass services via DI or parameters
class UserService {
constructor(
private logger: Logger,
private db: Database
) {}
async getUsers() {
this.logger.info('Fetching users')
// Use db
}
}
// In routes
app.get('/users', (req, res) => {
const logger = nsk.getContainer().resolve('logger')
logger.info('Request received')
})
```
### Step 6: Update Health Checks
**Before:**
```typescript
// Not available in v1
```
**After:**
```typescript
import { HealthChecker } from '@armco/node-starter-kit/v2'
const healthChecker = new HealthChecker(
nsk.getPluginManager(),
nsk.getConfig().health || {},
logger
)
app.use(healthChecker.createRouter())
// Available endpoints:
// GET /health - Full health check
// GET /health/live - Liveness probe
// GET /health/ready - Readiness probe
```
### Step 7: Update CSRF Protection
**Before:**
```typescript
// csurf middleware from config
```
**After:**
```typescript
import { initCsrf } from '@armco/node-starter-kit/v2'
initCsrf(app, {
enabled: process.env.NODE_ENV === 'production',
secret: process.env.CSRF_SECRET,
excludePaths: ['/health', '/auth/login']
}, logger)
// Client-side: Fetch /csrf-token and include in requests
```
### Step 8: Update Tests
**Before:**
```typescript
// Tests affected by global state
```
**After:**
```typescript
import { Application } from '@armco/node-starter-kit/v2'
describe('App', () => {
let nsk: Application
beforeEach(async () => {
nsk = await Application.create(express())
.withConfig({
appName: 'test-app',
env: 'test',
plugins: { logger: { level: 'silent' } }
})
.build()
})
afterEach(async () => {
await nsk.shutdown() // Clean shutdown
})
it('should work', () => {
const logger = nsk.getContainer().resolve('logger')
expect(logger).toBeDefined()
})
})
```
## Feature Parity
| Feature | v1 | v2 |
|---------|----|----|
| Logger (Winston) | ✅ | ✅ |
| Database (Mongoose) | ✅ | ✅ |
| Socket.IO | ✅ | 🚧 Coming soon |
| Cron Jobs | ✅ | 🚧 Coming soon |
| Helmet | ✅ | ✅ |
| CORS | ✅ | ✅ |
| JWT Auth | ✅ | ✅ (improved) |
| CSRF | ✅ (deprecated lib) | ✅ (modern) |
| Rate Limiting | ✅ | ✅ |
| Health Checks | ❌ | ✅ |
| Circuit Breaker | ❌ | ✅ |
| DI Container | ❌ | ✅ |
| Plugin System | ❌ | ✅ |
| TypeScript | Partial | Full |
## New Features in v2
### Circuit Breaker
```typescript
database: {
uri: process.env.MONGO_URI,
circuitBreaker: {
enabled: true,
threshold: 5,
timeout: 60000,
onOpen: () => logger.error('Circuit opened!')
}
}
```
### Custom Plugins
```typescript
class MyPlugin extends BasePlugin {
name = 'my-plugin'
version = '1.0.0'
async install(ctx) {
ctx.container.singleton('myService', new MyService())
}
}
```
### Health Checks
```typescript
// Kubernetes-ready probes
GET /health # Overall health
GET /health/live # Liveness probe
GET /health/ready # Readiness probe
```
## Troubleshooting
### "Cannot find module 'cosmiconfig'"
```bash
npm install cosmiconfig
```
### "No configuration found"
Create `armco.config.ts` in project root or specify path:
```typescript
.withConfig('./path/to/config.ts')
```
### "JWT_SECRET is required"
Set environment variable:
```bash
export JWT_SECRET="your-secret-key"
```
### "TypeScript files not loading"
Install tsx or ts-node:
```bash
npm install tsx
```
## Getting Help
- 📖 Read the [v2 README](./README.md)
- 💡 Check [examples](./examples/)
- 🐛 Report issues on [GitHub](https://github.com/ReStruct-Corporate-Advantage/node-starter-kit/issues)
- 💬 Ask questions in discussions
## Timeline
- **v1.x**: Maintenance mode (security fixes only)
- **v2.x**: Active development
- **v1 EOL**: 6 months after v2.0 release

735
docs/PLUGINS.md Normal file
View File

@@ -0,0 +1,735 @@
# Node Starter Kit v2 - Plugin Reference
Complete guide to all available plugins and their configurations.
---
## Core Plugins
### Logger Plugin
Winston-based logging with multiple transports and structured logging.
**Installation:**
```typescript
import { createLoggerPlugin } from '@armco/node-starter-kit/v2'
const nsk = await Application.create(app)
.plugin(createLoggerPlugin())
.build()
```
**Armco-Specific Configuration:**
```typescript
createLoggerPlugin({
enabled: true, // Enable/disable the plugin
level: 'info', // Log level: 'debug' | 'info' | 'warn' | 'error' | 'silent'
adapter: 'winston', // Logger adapter to use
format: 'json', // Output format: 'json' | 'pretty'
transports: [ // Array of transport configurations
{ type: 'console' },
{
type: 'file',
filename: 'logs/app.log',
maxsize: 5242880, // 5MB
maxFiles: 5
},
{
type: 'mongodb',
uri: process.env.MONGO_URI,
collection: 'logs'
},
{
type: 'http',
url: 'https://logs.example.com'
}
],
defaultMeta: { // Metadata added to all logs
app: 'my-app',
env: process.env.NODE_ENV
}
}
```
**Usage:**
```typescript
const logger = container.resolve('logger')
logger.info('User logged in', { userId: 123 })
logger.error('Failed to process', { error })
```
---
### Database Plugin
Mongoose-based MongoDB connection with circuit breaker pattern.
**Installation:**
```typescript
import { createDatabasePlugin } from '@armco/node-starter-kit/v2'
const nsk = await Application.create(app)
.plugin(createDatabasePlugin({ uri: process.env.MONGO_URI }))
.build()
```
**Armco-Specific Configuration:**
```typescript
createDatabasePlugin({
enabled: true,
adapter: 'mongoose', // Currently only 'mongoose' supported
uri: process.env.MONGO_URI, // MongoDB connection URI
options: { // Mongoose connection options
// See Mongoose documentation
},
circuitBreaker: {
enabled: true,
threshold: 5, // Open after 5 failures
timeout: 60000, // Try again after 60 seconds
onOpen: () => {
logger.error('Database circuit breaker opened')
},
onClose: () => {
logger.info('Database circuit breaker closed')
}
}
})
```
**Usage:**
```typescript
const database = container.resolve('database')
const isConnected = await database.isConnected()
```
---
### Cache Plugin
Flexible caching with Redis or in-memory adapters.
**Installation:**
```typescript
import { createCachePlugin } from '@armco/node-starter-kit/v2'
const nsk = await Application.create(app)
.plugin(createCachePlugin())
.build()
```
**Armco-Specific Configuration:**
```typescript
createCachePlugin({
enabled: true,
adapter: 'redis', // 'redis' | 'memory'
ttl: 3600, // Default TTL in seconds
maxItems: 1000, // Max items (memory adapter only)
uri: 'redis://localhost:6379', // Redis URI
keyPrefix: 'myapp:', // Prefix for all cache keys
circuitBreaker: { // Redis adapter only
enabled: true,
threshold: 5,
timeout: 30000,
onOpen: () => logger.error('Cache circuit breaker opened'),
onClose: () => logger.info('Cache circuit breaker closed')
},
serialization: {
enabled: true, // Auto-serialize/deserialize
serialize: (value) => JSON.stringify(value),
deserialize: (value) => JSON.parse(value)
}
})
```
**Usage:**
```typescript
const cache = container.resolve<CacheAdapter>('cache')
// Set
await cache.set('user:123', { name: 'John' }, 3600)
// Get
const user = await cache.get('user:123')
// Get or compute
const result = await cache.getOrSet('expensive', async () => {
return await computeExpensiveValue()
}, 3600)
// Increment
await cache.increment('page:views')
// Stats
const stats = await cache.stats()
```
---
### Socket.IO Plugin
Real-time WebSocket communication with Socket.IO.
**Installation:**
```typescript
import http from 'http'
import { createSocketPlugin } from '@armco/node-starter-kit/v2'
const server = http.createServer(app)
const nsk = await Application.create(app)
.plugin(createSocketPlugin(server))
.build()
```
**Armco-Specific Configuration:**
```typescript
createSocketPlugin(server, {
enabled: true,
options: { // Socket.IO server options
// See Socket.IO documentation
},
cors: {
origin: ['https://example.com'],
credentials: true
},
auth: {
enabled: true,
secret: process.env.JWT_SECRET,
secretProvider: () => process.env.JWT_SECRET
},
redis: { // Enable Redis adapter for clustering
enabled: true,
uri: 'redis://localhost:6379',
options: {}
},
handlers: {
connection: (socket) => {
console.log('Client connected:', socket.id)
},
namespaces: {
'/admin': (socket) => {
console.log('Admin connected:', socket.id)
}
}
},
middleware: [ // Socket.IO middleware
(socket, next) => {
// Custom middleware
next()
}
]
})
```
**Usage:**
```typescript
const socket = container.resolve<SocketAdapter>('socket')
// Emit to all clients
socket.emit('notification', { message: 'Hello!' })
// Emit to namespace
socket.emitTo('/admin', 'update', { data })
// Emit to room
socket.emitToRoom('room1', 'message', { text: 'Hi' })
// Register handlers
socket.onConnection((clientSocket) => {
clientSocket.on('chat', (data) => {
clientSocket.broadcast.emit('chat', data)
})
})
```
---
### OpenTelemetry Plugin
Distributed tracing and metrics using the OpenTelemetry standard.
**Installation:**
```typescript
import { createOpenTelemetryPlugin } from '@armco/node-starter-kit/v2'
const nsk = await Application.create(app)
.plugin(createOpenTelemetryPlugin({
serviceName: 'auth-core',
otlpEndpoint: 'http://tempo:4318'
}))
.build()
```
**Armco-Specific Configuration:**
```typescript
createOpenTelemetryPlugin({
enabled: true, // Enable/disable the plugin
serviceName: 'auth-core', // Required: Service name for traces
serviceVersion: '1.0.0', // Service version
// Exporters
exporters: ['otlp', 'console'], // 'otlp' | 'console' | 'jaeger' | 'zipkin'
otlpEndpoint: 'http://tempo:4318', // Grafana Tempo/OTLP endpoint
otlpHeaders: { // Optional auth headers
'Authorization': 'Bearer token'
},
// Features
autoInstrumentation: true, // Auto-instrument Express, MongoDB, Redis, etc.
enableTracing: true, // Enable distributed tracing
enableMetrics: true, // Enable metrics collection
sampleRate: 1.0, // Trace sample rate (0-1)
// Resource attributes (for filtering in Grafana)
resourceAttributes: {
'deployment.environment': 'production',
'service.namespace': 'iam',
'service.instance.id': process.env.HOSTNAME
},
// Advanced
disabledInstrumentations: [ // Disable specific instrumentations
'@opentelemetry/instrumentation-fs'
]
})
```
**Key Features:**
- ✅ Automatic instrumentation for Express, MongoDB, Redis, PostgreSQL
- ✅ Distributed tracing across microservices
- ✅ OTLP exporter for Grafana Tempo
- ✅ Jaeger and Zipkin exporters
- ✅ Metrics collection (counters, histograms, gauges)
- ✅ Dynamic imports (only loaded when enabled)
**Usage:**
```typescript
// Auto-instrumentation works automatically - no manual instrumentation needed!
app.get('/users', async (req, res) => {
// This route is automatically traced
const users = await db.users.find()
res.json(users)
})
// Manual instrumentation (optional)
const otel = container.resolve<OpenTelemetryAdapter>('opentelemetry')
const tracer = otel.getTracer()
const span = tracer.startSpan('complex-operation')
span.setAttribute('user.id', userId)
try {
await doWork()
span.setStatus({ code: 1 }) // OK
} catch (error) {
span.setStatus({ code: 2, message: String(error) }) // ERROR
throw error
} finally {
span.end()
}
// Custom metrics
const meter = otel.getMeter()
const counter = meter.createCounter('orders_processed')
counter.add(1, { 'order.type': 'premium' })
```
**View Traces in Grafana:**
1. Navigate to Grafana Explore → Tempo
2. Query: `{ service.name="auth-core" }`
3. View flame graphs and trace details
**For third-party properties**, refer to:
- [OpenTelemetry Node.js Documentation](https://opentelemetry.io/docs/instrumentation/js/)
- [OTLP Exporter Configuration](https://opentelemetry.io/docs/reference/specification/protocol/exporter/)
---
## Middleware Initialization
Middlewares are not plugins but utility functions to initialize Express middleware.
### Helmet (Security Headers)
**Usage:**
```typescript
import { initHelmet } from '@armco/node-starter-kit/v2'
initHelmet(app, {
enabled: true,
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"]
}
},
options: {
// See Helmet documentation
}
}, logger)
```
### CORS
**Usage:**
```typescript
import { initCors } from '@armco/node-starter-kit/v2'
initCors(app, {
enabled: true,
allowedOrigins: ['https://example.com'],
credentials: true,
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}, logger)
```
### CSRF Protection
**Usage:**
```typescript
import { initCsrf } from '@armco/node-starter-kit/v2'
initCsrf(app, {
enabled: true,
secret: process.env.CSRF_SECRET,
cookieName: '_csrf',
headerName: 'X-CSRF-Token',
cookieOptions: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict'
},
excludePaths: ['/auth/login', '/webhooks']
}, logger)
```
### JWT Authentication
**Usage:**
```typescript
import { initJwt, signToken } from '@armco/node-starter-kit/v2'
initJwt(app, {
enabled: true,
secretProvider: () => process.env.JWT_SECRET,
algorithms: ['RS256', 'HS256'], // Algorithm allowlist
issuer: 'my-app',
audience: 'my-api',
expiresIn: '1h',
tokenLocations: ['header', 'cookie'],
publicPaths: ['/health', '/auth/login']
}, logger)
// Sign a token
const token = signToken({ userId: 123 }, {
secret: process.env.JWT_SECRET,
expiresIn: '1h'
})
```
### Rate Limiting
**Usage:**
```typescript
import { initRateLimiter } from '@armco/node-starter-kit/v2'
initRateLimiter(app, {
enabled: true,
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Max requests per window
standardHeaders: true,
skipPaths: ['/health'],
message: 'Too many requests'
}, logger)
```
---
## Observability
### Health Checks
**Usage:**
```typescript
import { HealthChecker } from '@armco/node-starter-kit/v2'
const healthChecker = new HealthChecker(
nsk.getPluginManager(),
nsk.getConfig().health || {},
logger
)
// Add custom checks
healthChecker.registerCheck('redis', async () => {
const isConnected = await redis.ping()
return {
status: isConnected ? 'healthy' : 'unhealthy',
message: 'Redis connection',
timestamp: new Date()
}
})
// Mount routes
app.use(healthChecker.createRouter())
// GET /health - Overall health
// GET /health/live - Liveness probe
// GET /health/ready - Readiness probe
```
### Metrics Collection
**Usage:**
```typescript
import { initMetrics } from '@armco/node-starter-kit/v2'
const metrics = initMetrics(app, {
enabled: true,
endpoint: '/metrics',
labels: {
app: 'my-app',
env: process.env.NODE_ENV,
version: '1.0.0'
}
}, logger)
// Record custom metrics
metrics.incrementCounter('events_processed', { type: 'click' })
metrics.recordMetric('query_duration', 42, { table: 'users' })
// GET /metrics - JSON format
// GET /metrics/prometheus - Prometheus format
```
---
## Configuration Loading
**File-based configuration:**
Create `armco.config.ts`:
```typescript
export default {
appName: 'my-app',
env: process.env.NODE_ENV,
plugins: {
logger: {
level: 'info',
transports: [{ type: 'console' }]
},
database: {
uri: process.env.MONGO_URI
},
cache: {
adapter: 'redis',
uri: process.env.REDIS_URI
}
},
middlewares: {
helmet: { enabled: true },
cors: {
allowedOrigins: process.env.ALLOWED_ORIGINS?.split(',')
},
jwt: {
secretProvider: () => process.env.JWT_SECRET
}
},
health: {
enabled: true,
endpoint: '/health'
},
metrics: {
enabled: true,
endpoint: '/metrics',
labels: { app: 'my-app' }
},
server: {
port: 3000
}
})
```
**Programmatic configuration:**
```typescript
const nsk = await Application.create(app)
.withConfig({
appName: 'my-app',
// ... config
})
.build()
```
---
## Plugin Development
### Creating a Custom Plugin
```typescript
import { BasePlugin } from '@armco/node-starter-kit/v2'
import { ApplicationContext } from '@armco/node-starter-kit/v2'
export class MyPlugin extends BasePlugin {
constructor(config: MyConfig = {}) {
super('my-plugin', '1.0.0', ['logger'], config)
}
async install(context: ApplicationContext): Promise<void> {
await super.install(context)
const logger = context.container.resolve('logger')
logger.info('Installing MyPlugin')
// Register services
context.container.singleton('myService', new MyService())
}
async start(): Promise<void> {
// Initialize resources
}
async stop(): Promise<void> {
// Cleanup resources
}
async healthCheck(): Promise<HealthStatus> {
return {
status: 'healthy',
message: 'MyPlugin is operational',
timestamp: new Date()
}
}
}
export function createMyPlugin(config?: MyConfig) {
return new MyPlugin(config)
}
```
---
## Best Practices
### Plugin Order
Plugins are automatically ordered by dependencies, but you can specify priority:
```typescript
class MyPlugin extends BasePlugin {
readonly priority = 100 // Higher priority = installed first
}
```
### Environment Variables
Always use environment variables for secrets:
```typescript
// ❌ Don't hardcode secrets
const plugin = createPlugin({ secret: 'my-secret' })
// ✅ Use environment variables
const plugin = createPlugin({
secretProvider: () => {
const secret = process.env.SECRET
if (!secret) {
throw new Error('SECRET environment variable is required')
}
return secret
}
})
```
### Error Handling
Plugins should handle errors gracefully:
```typescript
async start(): Promise<void> {
try {
await this.resource.connect()
} catch (error) {
this.logger?.error('Failed to start plugin', { error })
throw error // Re-throw to prevent app startup
}
}
```
### Health Checks
Implement meaningful health checks:
```typescript
async healthCheck(): Promise<HealthStatus> {
try {
await this.database.ping()
const stats = await this.database.stats()
return {
status: 'healthy',
message: 'Database is connected',
details: {
connections: stats.connections,
responseTime: stats.responseTime
},
timestamp: new Date()
}
} catch (error) {
return {
status: 'unhealthy',
message: 'Database connection failed',
details: { error: String(error) },
timestamp: new Date()
}
}
}
```
---
## Examples
See the `v2/examples/` directory for complete working examples:
- `basic-usage.ts` - Simple setup
- `with-metrics.ts` - Metrics and observability
- `with-socket-io.ts` - Real-time communication
- `with-cache.ts` - Caching strategies
---
## Troubleshooting
### Plugin Not Found
```
Error: Service 'myPlugin' not found in container
```
**Solution:** Make sure the plugin is registered before trying to resolve it:
```typescript
.plugin(createMyPlugin()) // Register first
```
### Circular Dependencies
```
Error: Circular dependency detected involving 'pluginA'
```
**Solution:** Refactor to remove circular dependencies or use lazy loading.
### Type Errors
```
Error: Cannot find module '@armco/node-starter-kit/v2'
```
**Solution:** Install dependencies:
```bash
npm install @armco/node-starter-kit
```
---
For more information, see:
- [README.md](./README.md) - Getting started
- [MIGRATION_GUIDE.md](./MIGRATION_GUIDE.md) - Upgrading from v1
- [ERROR_HANDLING.md](./ERROR_HANDLING.md) - Common errors and solutions

537
docs/README.md Normal file
View File

@@ -0,0 +1,537 @@
# Node Starter Kit v2.0
> Modern plugin-based starter kit for Node.js applications with TypeScript, security, and observability built-in.
## 🎉 What's New in v2
- **Plugin Architecture**: Modular, extensible design with lifecycle hooks
- **Dependency Injection**: Type-safe container for service management
- **Modern Security**: Replaces deprecated `csurf` with secure double-submit cookie pattern
- **Algorithm Allowlist**: JWT authentication with enforced secure algorithms
- **Circuit Breaker**: Built-in circuit breaker for database connections
- **Health Checks**: Kubernetes-ready liveness and readiness probes
- **TypeScript First**: No `any` types, full type safety
- **Zero Global Pollution**: DI container replaces global state
- **Graceful Shutdown**: Proper cleanup of resources on termination
- **Configuration Validation**: Runtime validation with helpful error messages
## 📦 Installation
```bash
npm install @armco/node-starter-kit
```
## 🚀 Quick Start
```typescript
import express from 'express'
import { Application } from '@armco/node-starter-kit/v2'
import { createLoggerPlugin } from '@armco/node-starter-kit/v2'
const app = express()
// Method 1: With configuration file (armco.config.ts)
const nsk = await Application.create(app)
.withConfig('./armco.config.ts') // Auto-discovers if not provided
.build()
// Method 2: Inline configuration
const nsk = await Application.create(app)
.withConfig({
appName: 'my-app',
plugins: {
logger: { level: 'info', format: 'pretty' }
}
})
.plugin(createLoggerPlugin())
.build()
// Get services from DI container
const logger = nsk.getContainer().resolve('logger')
app.get('/', (req, res) => {
logger.info('Request received')
res.json({ message: 'Hello World' })
})
app.listen(3000, () => {
logger.info('Server started on port 3000')
})
```
## 🔧 Configuration
Create `armco.config.ts` in your project root:
```typescript
export default {
appName: 'my-app',
plugins: {
logger: {
level: 'info',
adapter: 'winston',
format: 'pretty',
transports: [
{ type: 'console' },
{ type: 'file', filename: 'app.log' }
]
},
database: {
adapter: 'mongoose',
uri: process.env.MONGO_URI,
circuitBreaker: {
enabled: true,
threshold: 5,
timeout: 60000
}
}
},
middlewares: {
helmet: {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"]
}
}
},
cors: {
allowedOrigins: ['http://localhost:3000'],
credentials: true
},
rateLimiter: {
windowMs: 15 * 60 * 1000,
max: 100
},
jwt: {
secretProvider: () => process.env.JWT_SECRET,
algorithms: ['RS256', 'HS256'],
publicPaths: ['/health', '/auth/login']
}
},
health: {
enabled: true,
endpoint: '/health'
}
}
```
## 🔌 Plugins
### Logger Plugin
```typescript
import { createLoggerPlugin } from '@armco/node-starter-kit/v2'
nsk.plugin(createLoggerPlugin({
level: 'info',
format: 'json',
transports: [
{ type: 'console' },
{ type: 'file', filename: 'app.log', maxsize: 10485760 },
{ type: 'mongodb', uri: process.env.MONGO_LOG_URI, collection: 'logs' }
]
}))
// Usage
const logger = nsk.getContainer().resolve('logger')
logger.info('Application started', { version: '1.0.0' })
logger.error('Error occurred', { error })
```
### Database Plugin
```typescript
import { createDatabasePlugin } from '@armco/node-starter-kit/v2'
nsk.plugin(createDatabasePlugin({
adapter: 'mongoose',
uri: process.env.MONGO_URI,
options: {
retryWrites: true
},
circuitBreaker: {
enabled: true,
threshold: 5,
timeout: 60000,
onOpen: () => logger.error('Circuit breaker opened!')
}
}))
// Usage
const db = nsk.getContainer().resolve('database')
```
### Custom Plugin
```typescript
import { BasePlugin, ApplicationContext } from '@armco/node-starter-kit/v2'
class EmailPlugin extends BasePlugin {
name = 'email'
version = '1.0.0'
dependencies = ['logger']
async install(context: ApplicationContext) {
await super.install(context)
const emailService = new EmailService(this.config)
context.container.singleton('email', emailService)
this.getLogger()?.info('Email plugin installed')
}
async start() {
this.getLogger()?.info('Email plugin started')
}
async stop() {
this.getLogger()?.info('Email plugin stopped')
}
}
nsk.plugin(new EmailPlugin({ apiKey: process.env.SENDGRID_KEY }))
```
### Bring Your Own Services (withServices)
For advanced use cases where you need full control over service initialization (like databases), use `withServices()`:
```typescript
import { PrismaClient } from '@prisma/client'
import { Application } from '@armco/node-starter-kit/v2'
// You own the database initialization
const prisma = new PrismaClient({
datasources: { db: { url: process.env.DATABASE_URL } },
log: ['query', 'error']
})
await prisma.$connect()
// Register services directly in the DI container
const nsk = await Application.create(app)
.withServices((container) => {
// Register database
container.singleton('database', prisma)
// Register repositories
container.singleton('userRepo', new UserRepository(prisma))
// Register any custom services
container.singleton('config', { maxRetries: 3 })
})
.plugin(createLoggerPlugin())
.build()
// Services available via DI
const db = nsk.getContainer().resolve<PrismaClient>('database')
const userRepo = nsk.getContainer().resolve('userRepo')
```
**When to use `withServices()`:**
- Complex database requirements (auth systems, multi-tenancy)
- Need full control over ORM/database client
- Using advanced features (Prisma migrations, Drizzle schemas)
- Existing codebase with established patterns
**When to use NSK plugins:**
- Quick prototypes and MVPs
- Standard use cases
- Want batteries-included approach
See [`examples/with-custom-db.ts`](./examples/with-custom-db.ts) for complete example.
## 🛡️ Security Middlewares
### Helmet (Security Headers)
```typescript
import { initHelmet } from '@armco/node-starter-kit/v2'
initHelmet(app, {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com']
}
}
}, logger)
```
### CORS
```typescript
import { initCors } from '@armco/node-starter-kit/v2'
initCors(app, {
allowedOrigins: process.env.ALLOWED_ORIGINS.split(','),
credentials: true,
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE']
}, logger)
```
### CSRF Protection (Modern)
Replaces deprecated `csurf` with secure double-submit cookie pattern:
```typescript
import { initCsrf } from '@armco/node-starter-kit/v2'
initCsrf(app, {
enabled: process.env.NODE_ENV === 'production',
secret: process.env.CSRF_SECRET,
cookieOptions: {
httpOnly: true,
secure: true,
sameSite: 'strict'
},
excludePaths: ['/health', '/auth/login']
}, logger)
// Client usage:
// 1. GET /csrf-token to receive token
// 2. Include token in X-CSRF-Token header for POST/PUT/DELETE
```
### JWT Authentication
```typescript
import { initJwt } from '@armco/node-starter-kit/v2'
initJwt(app, {
secretProvider: async () => {
// Load from vault, environment, etc.
return process.env.JWT_SECRET
},
algorithms: ['RS256', 'ES256'], // Secure algorithms only
issuer: 'my-app',
expiresIn: '1h',
publicPaths: ['/health', '/auth/*']
}, logger)
// Authenticated routes automatically have req.user and req.token
app.get('/protected', (req, res) => {
res.json({ user: req.user })
})
```
### Rate Limiter
```typescript
import { initRateLimiter } from '@armco/node-starter-kit/v2'
initRateLimiter(app, {
windowMs: 15 * 60 * 1000,
max: 100,
keyGenerator: (req) => req.ip,
skipPaths: ['/health', '/metrics']
}, logger)
```
## 🏥 Health Checks
```typescript
import { HealthChecker } from '@armco/node-starter-kit/v2'
const healthChecker = new HealthChecker(
nsk.getPluginManager(),
nsk.getConfig().health || {},
logger
)
// Register custom health check
healthChecker.registerCheck('redis', async () => {
const isConnected = await redis.ping()
return {
status: isConnected ? 'healthy' : 'unhealthy',
responseTime: Date.now()
}
})
// Add routes
app.use(healthChecker.createRouter())
// Endpoints:
// GET /health - Full health check
// GET /health/live - Liveness probe (always 200 if running)
// GET /health/ready - Readiness probe (200 if ready for traffic)
```
## 📊 Dependency Injection
```typescript
// Register services
nsk.getContainer().singleton('cache', new RedisCache())
nsk.getContainer().transient('requestId', () => uuid())
// Resolve services
const cache = nsk.getContainer().resolve('cache')
const logger = nsk.getContainer().resolve('logger')
// Check if service exists
if (nsk.getContainer().has('database')) {
const db = nsk.getContainer().resolve('database')
}
```
## 🔄 Graceful Shutdown
Automatic handling of SIGTERM and SIGINT:
```typescript
// Plugins' stop() methods are called in reverse dependency order
process.on('SIGTERM', async () => {
await nsk.shutdown()
process.exit(0)
})
```
## 🧪 Testing
### Comprehensive Test Suite
NSK v2 includes a production-grade testing infrastructure with:
- ✅ 82 tests (Unit + Integration)
- ✅ Exhaustive configuration testing
- ✅ Real host application simulation
- ✅ 80% coverage requirement
- ✅ Visual test UI
### Run Tests
```bash
# Run all tests
npm test
# Visual UI
npm run test:ui
# Coverage report
npm run test:coverage
# Unit tests only
npm run test:unit
# Integration tests only
npm run test:integration
```
### Test Your App
```typescript
import { Application } from '@armco/node-starter-kit/v2'
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import express from 'express'
import request from 'supertest'
describe('My App', () => {
let app: express.Express
let nsk: Application
beforeEach(async () => {
app = express()
nsk = await Application.create(app)
.withConfig({
appName: 'test-app',
env: 'test',
plugins: {
logger: { level: 'silent' }
}
})
.build()
app.get('/api/test', (req, res) => res.json({ ok: true }))
})
afterEach(async () => {
await nsk.shutdown()
})
it('should start successfully', () => {
expect(nsk.getContainer().has('logger')).toBe(true)
})
it('should handle requests', async () => {
const response = await request(app).get('/api/test')
expect(response.status).toBe(200)
expect(response.body.ok).toBe(true)
})
})
```
### Exhaustive Config Testing
Test files include exhaustive configurations that validate every option:
- `all-middlewares.config.ts` - Every security middleware
- `all-plugins.config.ts` - Every plugin with all options
- `production-like.config.ts` - Real production scenarios
See [`__tests__/TEST_INFRASTRUCTURE_SUMMARY.md`](./__tests__/TEST_INFRASTRUCTURE_SUMMARY.md) for details.
## 📝 Migration from v1
### Breaking Changes
1. **Global namespace removed** - Use DI container
2. **Config file format changed** - `armcorc.json``armco.config.ts`
3. **Initialization API changed** - See examples above
4. **CSRF library replaced** - New double-submit cookie pattern
### Migration Steps
```typescript
// v1 (OLD)
import initNodeStarterKit from '@armco/node-starter-kit'
initNodeStarterKit(app, {
supersedeModules: { LOG: true, DB: true }
})
global.logger.info('Hello')
// v2 (NEW)
import { Application, createLoggerPlugin } from '@armco/node-starter-kit/v2'
const nsk = await Application.create(app)
.plugin(createLoggerPlugin())
.build()
const logger = nsk.getContainer().resolve('logger')
logger.info('Hello')
```
## 🔒 Security Improvements
- ✅ Removed deprecated `csurf` package
- ✅ JWT algorithm allowlist (no "none" algorithm)
- ✅ Enforced environment variables for secrets
- ✅ Circuit breaker for database connections
- ✅ Modern CSRF with double-submit cookies
- ✅ Updated dependencies (Mongoose 8, Helmet 7)
## 📚 Examples
See `/v2/examples` directory for:
- Basic usage
- Custom plugins
- Configuration examples
- Testing patterns
## 🤝 Contributing
Contributions welcome! Please read CONTRIBUTING.md first.
## 📄 License
ISC License - see LICENSE file for details
## 🙏 Acknowledgments
Built with love by the Armco team for the Node.js community.

View File

@@ -1,6 +1,6 @@
{
"name": "@armco/node-starter-kit",
"version": "2.1.1",
"version": "2.1.2",
"description": "Modern plugin-based starter kit for Node.js applications with TypeScript, security, and observability",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -279,15 +279,18 @@ export class ApplicationBuilder {
private setupGlobalNamespace(config: AppConfig): void {
const appName = config.appName.replace(/[-\s]/g, '_').toUpperCase()
const globalObj = global as Record<string, unknown>
const rawConfig = config as unknown as Record<string, unknown>
// Create namespace with config sections
globalObj[appName] = {
appConfig: rawConfig.APP_CONFIG || {},
appConfig: config.APP_CONFIG || {},
config: config,
keys: rawConfig.KEYS || {},
modules: rawConfig.modules || {},
keys: config.KEYS || {},
modules: config.modules || {},
env: config.env || process.env.NODE_ENV || 'development',
server: {
port: config.server?.port || 8081,
host: config.server?.host || '0.0.0.0',
},
}
}

View File

@@ -37,6 +37,16 @@ export interface AppConfig {
health?: HealthConfig
metrics?: MetricsConfig
server?: ServerConfig
/** Application-specific configuration (custom per app) */
APP_CONFIG?: Record<string, unknown>
/** Secrets and API keys */
KEYS?: Record<string, unknown>
/** Module configurations */
modules?: Record<string, unknown>
/** Allow additional properties for extensibility */
[key: string]: unknown
}
/**