@richardpickett/node-app
v1.7.5
Published
Base node app that utilizes logging, async dependencies, and commander
Readme
Node App
A powerful Node.js application framework that provides built-in support for components, commands, logging, configuration, and more.
Installation
npm install @richardpickett/node-appQuick Start
The easiest way to get started is to copy the example app:
# Extract the example app
node-app_example-app.sh
# Or manually copy the example-app directory
cp -r node_modules/@richardpickett/node-app/example-app my-app
cd my-app
npm installThe example app provides a complete working application with the following structure:
my-app/
├── src/
│ ├── commands/ # Command definitions
│ │ ├── index.js # Command registration
│ │ ├── globals.js # Global command options
│ │ └── myCommand.js # Individual commands
│ ├── components/ # Custom components
│ │ ├── index.js # Component registration
│ │ └── myComponent.js # Individual components
│ ├── lib/ # Application-specific code
│ │ ├── application.js # Application instance
│ │ └── initialize.js # Initialization logic
│ └── index.js # Application entry point
├── .env # Environment variables
└── package.jsonBasic Usage
Application Entry Point (src/index.js)
import application from "./lib/application.js";
import registerComponents from "./components/index.js";
import registerCommands from "./commands/index.js";
import initialize from "./lib/initialize.js";
async function run() {
// Register components first
registerComponents(application);
// Then register commands and run the application
return registerCommands(application)
.then(() => initialize())
.then(() => application.run())
.then((result) => process.exit(result ? 0 : 1));
}
run();Components
Components are reusable pieces of functionality that can be loaded on demand. They are registered using registerComponent() and loaded using loadComponents().
Creating a Component (src/components/myComponent.js)
class MyComponent {
constructor(config) {
this.config = config;
}
doSomething() {
return "Hello from my component!";
}
}
export default function registerMyComponent(application) {
return application.registerComponent("myComponent", async () => {
// Load dependencies first
return application.loadComponents("config").then(({ config }) => new MyComponent(config));
});
}Registering Components (src/components/index.js)
import registerMyComponent from "./myComponent.js";
export default function registerComponents(application) {
registerMyComponent(application);
}Using Components
// Load a single component
application.loadComponents("myComponent").then(({ myComponent }) => {
console.log(myComponent.doSomething());
});
// Load multiple components
application.loadComponents(["config", "logger"]).then(({ config, logger }) => {
// Use components here
});
// Alternately, you can load multiple components without the array:
application.loadComponents("config", "logger").then(({ config, logger }) => {
// Use components here
});
// Load components with error handling, if the last parameter is boolean, it indicates an error should not be thrown if a component isn't registered, but instead return false for that component
application
.loadComponents("myComponent", true) // true enables quiet fail mode
.then((components) => {
if (components.myComponent) {
console.log(components.myComponent.doSomething());
}
});Commands
Commands are registered using registerCommand() and can be executed from the command line.
Creating a Command (src/commands/myCommand.js)
import application from "../lib/application.js";
export default async function registerMyCommand(commander) {
commander
.command("my-command")
.description("Execute my custom command")
.option("-f, --file <path>", "Path to file")
.action((options) => {
return myCommand(options);
});
}
async function myCommand(options) {
// Load required components
return application.loadComponents(["config", "logger"]).then(({ config, logger }) => {
// Command logic here
logger.info("Executing my command with options:", options);
return true;
});
}Registering Commands (src/commands/index.js)
import registerMyCommand from "./myCommand.js";
import registerGlobals from "./globals.js";
export default async function registerCommands(application) {
return application.loadComponents("commander").then(({ commander }) => {
// Register global options first
registerGlobals(commander);
// Then register specific commands
registerMyCommand(commander);
});
}Global Command Options (src/commands/globals.js)
import { Option } from "@richardpickett/node-app";
import application from "../lib/application.js";
export default async function registerGlobals(commander) {
return application.loadComponents("config").then(({ config }) => {
// Set default values
config.globalA = "A";
config.globalB = "B";
// Add global options
commander.addOption(new Option("-A, --global-a <a>", "global a setting").choices(["a", "b"]).default("a", "a"));
commander.option("-B, --global-b <b>", "global b setting", "b");
// Hook into command execution
commander.hook("preAction", (thisCommand, actionCommand) => {
loadGlobals(thisCommand, config);
});
});
}
function loadGlobals(commander, config) {
// Update config with command line options
config.globalA = commander.opts().globalA;
config.globalB = commander.opts().globalB;
}Using Commands
# Run a command
node src/index.js my-command
# Run with options
node src/index.js my-command --file ./data.json
# Run with global options
node src/index.js my-command --global-a b --global-b c
# Run multiple commands
node src/index.js my-command another-commandBuilt-in Components
Logger Component
The logger component provides structured logging capabilities:
application.loadComponents("logger").then(({ logger }) => {
// Log messages
logger.info("Application started");
logger.error("An error occurred", new Error("Test error"));
logger.debug("Debug information");
});
// Set log level
process.env.LOG_LEVEL = "debug"; // Options: error, warn, info, debugConfig Component
The config component manages application configuration:
application.loadComponents("config").then(({ config }) => {
// Set configuration
config.set("app.name", "My App");
config.set("app.version", "1.0.0");
// Get configuration
const appName = config.get("app.name");
const appVersion = config.get("app.version");
});Commander Component
The commander component handles command-line argument parsing:
application.loadComponents("commander").then(({ commander }) => {
// Set version
commander.version("1.0.0");
// Add global options
commander.option("-e, --env <environment>", "Set environment", "development");
// Parse arguments
commander.parse(process.argv);
});Initializers
Initializers are functions that run during application startup:
// Register an initializer
application.registerInitializer("setup", async () => {
// Perform setup tasks
console.log("Setting up application...");
return true; // Return true to indicate success
});
// Register multiple initializers
application.registerInitializer("validate", async () => {
// Validate configuration
return true;
});
application.registerInitializer("connect", async () => {
// Connect to database
return true;
});Error Handling
The framework provides several built-in error classes:
import {
NodeAppBaseError,
NodeAppDuplicateComponentError,
NodeAppDuplicateInitializerError,
NodeAppInitializerFailedError,
NodeAppComponentNotFoundError,
NodeAppInvalidComponentStateError,
} from "@richardpickett/node-app";
// Example error handling
application.loadComponents("nonExistentComponent").catch((error) => {
if (error instanceof NodeAppComponentNotFoundError) {
console.error("Component not found:", error.message);
}
});Configuration Files
.env
LOG_LEVEL=debug
DB_PASSWORD=secret
API_KEY=abc123
jsonVariables=["A_JSON_ENV_VAR"]
A_JSON_ENV_VAR={"a":1,"b":2}Best Practices
Component Organization:
- Place component definitions in
src/components/ - Use index.js for component registration
- Keep components focused and single-purpose
- Load dependencies in component registration
- Place component definitions in
Command Organization:
- Place command definitions in
src/commands/ - Use index.js for command registration
- Group related commands in separate files
- Register global options before specific commands
- Use preAction hooks for global option handling
- Place command definitions in
Configuration:
- Use
.envfor sensitive data - Set default values in global command options
- Use
Error Handling:
- Use built-in error classes for consistent error handling
- Implement proper error recovery in components
- Log errors with appropriate context
- Return true/false from commands to indicate success/failure
Initialization:
- Register components before commands
- Register initializers in order of dependency
- Return true/false to indicate success/failure
- Handle initialization errors gracefully
