Configuration
Zelt provides a type-safe configuration system using the @Config decorator and inject() helper.
Defining Configuration
Use the @Config decorator to define a configuration class. Each config class must have a static Token property:
import { Config, Env, inject } from '@zeltjs/core';
@Config
export class DatabaseConfig {
static readonly Token = DatabaseConfig;
constructor(private env = inject(Env)) {}
get host() {
return this.env.getString('DATABASE_HOST', 'localhost');
}
get port() {
return this.env.getNumber('DATABASE_PORT', 5432);
}
get connectionString() {
return `postgres://${this.host}:${this.port}/mydb`;
}
}
Using Configuration
Inject configuration into services or controllers using inject():
@Injectable()
export class DatabaseService {
constructor(private config = inject(DatabaseConfig)) {}
connect() {
return this.config.connectionString;
}
}
Registering Configuration
Register config classes when creating the app:
const app = createApp([http({
controllers: [AppController],
})], { configs: [DatabaseConfig] });
Overriding Configuration
Override configuration values for testing by extending the config class:
@Config
export class TestDatabaseConfig extends DatabaseConfig {
override get host() {
return 'test-db';
}
override get port() {
return 5433;
}
}
// In test setup
const app = createApp([http({
controllers: [AppController],
})], { configs: [TestDatabaseConfig] });
The Token property is inherited from the parent class, so inject(DatabaseConfig) will receive the overridden TestDatabaseConfig instance.
Abstract Configuration
Use @Config({ abstract: true }) to declare a config base class with no default
implementation. This is useful for a config contract that every environment
must supply a concrete value for — there is no sensible fallback:
const app = createApp([], { configs: [StripeConfig] });
If an abstract config is registered without a concrete subclass resolving it —
whether left unregistered, passed directly in configs, or only resolved by an
abstract subclass — createRuntime() (or the first inject() of the token)
throws a ZeltAppConfigurationError with reason abstract_leaf_without_concrete.
Fallback Configuration
createRuntime({ fallbackConfigs }) registers config subclasses that apply only
when nothing else resolves the base config. Resolution priority, from highest
to lowest:
createRuntime({ configs })— runtime overridecreateApp([...], { configs })— user-providedcreateRuntime({ fallbackConfigs })— fallback- The base config class's own default getter value
fallbackConfigs is commonly used to satisfy an abstract config with a
development-only default while requiring production code to pass a concrete
configs entry explicitly:
const app = createApp([]);
const readyApp = await app.createRuntime({
fallbackConfigs: [DevPaymentGatewayConfig],
});
Environment-Based Configuration
inject(Env) reads environment variables from the platform-specific source registered by the adapter. No additional setup is needed for the common case.
Node.js Environment
When using onNode(), ProcessEnvAdaptor is registered automatically, so inject(Env) reads from process.env without any extra config:
@Config
export class DatabaseConfig {
static readonly Token = DatabaseConfig;
constructor(private env = inject(Env)) {}
get host() {
return this.env.getString('DATABASE_HOST', 'localhost');
}
get port() {
return this.env.getNumber('DATABASE_PORT', 5432);
}
get connectionString() {
return `postgres://${this.host}:${this.port}/mydb`;
}
}
const app = createApp([http({
controllers: [AppController],
})], { configs: [DatabaseConfig] });
Loading .env Files
To load a .env file, import dotenv/config at the entry point of your application before anything else:
import 'dotenv/config';import { onNode } from '@zeltjs/adapter-node';
// ...rest of app setup
inject(Env) will then read the variables populated by dotenv from process.env.
Cloudflare Workers Environment
For Cloudflare Workers, environment configuration is handled automatically by onCloudflareWorkers(). See the Cloudflare Workers Getting Started guide for details.
TypeScript Decorator Configuration
Zelt supports both TC39 standard decorators and legacy TypeScript decorators. The framework automatically detects which mode is being used at runtime.
TC39 Standard Decorators (Recommended)
For new projects, use TC39 standard decorators. No special TypeScript configuration is needed:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext"
}
}
Legacy Decorators
For compatibility with existing codebases, enable legacy decorators:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"experimentalDecorators": true
}
}
Detection Behavior
Zelt automatically detects the decorator mode based on the runtime context:
- TC39 mode: Decorator receives a context object with
kind,name, andmetadataproperties - Legacy mode: Decorator receives
target,propertyKey, anddescriptorarguments
Both modes work identically from an API perspective—you don't need to change your code when switching between them.