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
All checks were successful
armco-org/node-starter-kit/pipeline/head This commit looks good
This commit is contained in:
344
docs/CONFIG_DRIVEN_BREAKING_CHANGE.md
Normal file
344
docs/CONFIG_DRIVEN_BREAKING_CHANGE.md
Normal 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
|
||||
372
docs/CONFIG_DRIVEN_INITIALIZATION.md
Normal file
372
docs/CONFIG_DRIVEN_INITIALIZATION.md
Normal 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
465
docs/ERROR_HANDLING.md
Normal 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
370
docs/GLOBAL_LOGGER.md
Normal 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
482
docs/MIGRATION_GUIDE.md
Normal 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
735
docs/PLUGINS.md
Normal 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
537
docs/README.md
Normal 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.
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user