Skip to main content

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:

import { Config, Env, inject } from '@zeltjs/core';

@Config
export class 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] });

inject(DatabaseConfig) resolves to the overriding TestDatabaseConfig instance because the subclass is registered for the parent's class token.

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:

  1. createRuntime({ configs }) — runtime override
  2. createApp([...], { configs }) — user-provided
  3. createRuntime({ fallbackConfigs }) — fallback
  4. 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 {
  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.

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, and metadata properties
  • Legacy mode: Decorator receives target, propertyKey, and descriptor arguments

Both modes work identically from an API perspective—you don't need to change your code when switching between them.