@nodeboot/config provides the configuration foundation for Node-Boot applications.
It loads application configuration from the same app-config.yaml / app-config.local.yaml conventions used across this repository, exposes a strongly-typed ConfigService, supports runtime reload notifications, and lets you bind configuration sections into injectable classes with @ConfigurationProperties().
Under the hood it builds on Backstage's config loader/reader stack, which means YAML files, environment variable placeholders, includes, local overrides, and remote config targets all work together cleanly.
✅ Typed config access via ConfigService (getString, getNumber, getBoolean, etc.)
✅ Grouped config binding with @ConfigurationProperties()
✅ Nested config views with getConfig() and getOptionalConfig()
✅ YAML-first workflow using app-config.yaml and app-config.local.yaml
✅ Environment placeholder support such as ${SUPABASE_URL} and ${BACKSTAGE_API_KEY:-dummy}
✅ Config includes/local overrides like $include: app-credentials.local.yaml
✅ Runtime reload notifications through ConfigService.subscribe()
✅ Remote or file-based config targets through loadNodeBootConfig()
✅ Simple in-memory config creation for tests or ad-hoc bootstrapping via loadConfig()
pnpm add @nodeboot/configThis package exports:
ConfigurationPropertiesConfigServiceloadNodeBootConfigloadConfigConfigurationPropertiesMetadata
A real pattern used in the repo looks like this:
app:
name: "facts-service"
platform: "tech-insights"
environment: "development"
defaultErrorHandler: false
port: 3000
server:
cors:
origin: "*"You can also use environment placeholders and local overrides:
integrations:
supabase:
url: ${SUPABASE_URL}
serviceRoleKey: ${SUPABASE_SERVICE_ROLE_KEY}credentials:
$include: app-credentials.local.yamlThis is the exact pattern used in the sample applications:
import {ConfigurationProperties} from "@nodeboot/config";
@ConfigurationProperties({
configPath: "app",
configName: "app-config",
})
export class AppConfigProperties {
name: string;
platform: string;
environment: string;
defaultErrorHandler: boolean;
customErrorHandler?: boolean;
port: number;
}configPath points to the YAML section to bind, and configName is the bean name registered in the IoC container.
A real sample in this repository injects the config bean by name:
import {Controller, Get} from "@nodeboot/core";
import {Inject} from "@nodeboot/di";
import {AppConfigProperties} from "../config/AppConfigProperties";
@Controller("/users", "v1")
export class UserController {
constructor(
@Inject("app-config")
private readonly appConfigProperties: AppConfigProperties,
) {}
@Get("/")
async getUsers(): Promise<string> {
return `Running on ${this.appConfigProperties.name}:${this.appConfigProperties.port}`;
}
}Use this approach when you want a whole config section as a structured object.
For one-off reads, inject ConfigService directly:
import {Service} from "@nodeboot/core";
import {ConfigService} from "@nodeboot/config";
@Service()
export class UserService {
constructor(private readonly configService: ConfigService) {}
getAppName(): string {
return this.configService.getString("app.name");
}
getPort(): number | undefined {
return this.configService.getOptionalNumber("app.port");
}
}Available typed accessors include:
get()/getOptional()getString()/getOptionalString()getNumber()/getOptionalNumber()getBoolean()/getOptionalBoolean()getStringArray()/getOptionalStringArray()getConfig()/getOptionalConfig()getConfigArray()/getOptionalConfigArray()has()keys()subscribe()
import {ConfigService} from "@nodeboot/config";
export class PersistenceInspector {
constructor(private readonly config: ConfigService) {}
read(): string | undefined {
const persistence = this.config.getOptionalConfig("persistence");
return persistence?.getOptionalString("type");
}
}getConfig("...") returns a child ConfigService view, so nested reads stay clean and explicit.
loadConfig() is useful for tests, local tooling, or manual bootstrapping:
import {loadConfig} from "@nodeboot/config";
const config = loadConfig({
app: {
name: "demo-service",
port: 3000,
},
});
console.log(config.getString("app.name")); // demo-service
console.log(config.getNumber("app.port")); // 3000loadNodeBootConfig() is the lower-level async loader used by Node-Boot server bootstrap.
import {loadNodeBootConfig} from "@nodeboot/config";
const {config} = await loadNodeBootConfig({
argv: process.argv,
additionalConfigData: {
app: {
environment: "development",
},
},
});It parses --config arguments, accepts local file paths or URLs, loads repository config files, and merges additionalConfigData on top.
loadNodeBootConfig() in src/service/config.ts:
- parses CLI arguments with
minimist - turns each
--configvalue into either a{path}or{url}target - uses
findPaths(__dirname)from@backstage/cli-commonto locate the project root - calls
loadConfig()from@backstage/config-loader - enables watch mode so config reloads can be observed at runtime
- merges file-based configs with optional
additionalConfigData - stores the final merged result in a
ConfigService
When watched files change, it rebuilds the merged ConfigReader and calls ConfigService.setConfig(...), which notifies subscribers.
ConfigService in src/service/ConfigService.ts:
- implements the
Configcontract from@nodeboot/context - wraps a Backstage
ConfigReader - exposes typed getters for strings, numbers, booleans, arrays, and nested objects
- supports
subscribe()for reload notifications - creates read-only child views through
getConfig()/getOptionalConfig() - forwards subscriptions from child views back to the root config instance
ConfigurationProperties() in src/decorator/ConfigurationProperties.ts:
- writes metadata (
config:isConfigPropertiesandconfig:path) on the decorated class - registers a
ConfigurationPropertiesAdapterinApplicationContext.get().configurationPropertiesAdapters - during binding, resolves the root
configbean from the IoC container - reads the object at
args.configPath - creates an instance of the decorated class
- copies enumerable config properties onto that instance
- registers the populated instance under
args.configName
If the config section is missing, or if another bean already uses the same configName, the adapter throws immediately.
- Use
ConfigServicewhen you want individual config values or dynamic path access. - Use
@ConfigurationProperties()when you want a whole section as a typed injectable object. - Use
loadConfig()for in-memory/test scenarios. - Use
loadNodeBootConfig()when you are building or customizing bootstrap/loading behavior.
If a decorated properties class points at a path that does not exist, binding fails with an error like:
Configuration for prefix 'app' not found.
Make sure configPath matches the YAML structure exactly.
If two @ConfigurationProperties() classes register the same configName, startup fails.
Use unique bean names for each bound config object.
Methods like getString(), getNumber(), and getBoolean() are strict by design.
If a value is optional, prefer:
getOptionalString()getOptionalNumber()getOptionalBoolean()getOptionalConfig()
Instances returned by getConfig() are scoped views over the parent config. They forward reads and subscriptions to the parent rather than owning separate state.
@nodeboot/config gives Node-Boot applications a practical, strongly-typed configuration layer with YAML loading, env placeholder support, nested config access, runtime reload notifications, and object binding through @ConfigurationProperties().
If you want Spring-Boot-style configuration organization in Node-Boot, this package is the core building block.