Enhanced Configuration
ExpressoTS v4 introduces an enhanced configuration system that provides type-safe configuration management with helpful validation, automatic environment switching, and secure secret handling.
Overview
The enhanced configuration system provides:
- Type-Safe Config: Full TypeScript inference on configuration values
- Helpful Validation: Detailed error messages with examples and hints
- Environment Switching: Automatic config switching based on environment
- Secret Management:
SecretValueclass for secure handling - Environment Variables: Type-safe access to environment variables
Defining Configuration
Use defineConfig to create a type-safe configuration:
// config/app.config.ts
import { defineConfig, Env } from "@expressots/core";
export default defineConfig({
app: {
name: Env.string("APP_NAME").default("My API"),
port: Env.number("PORT").default(3000),
environment: Env.string("NODE_ENV").default("development"),
},
database: {
url: Env.string("DATABASE_URL").required(),
pool: Env.number("DB_POOL_SIZE").default(10),
ssl: Env.boolean("DB_SSL").default(false),
},
features: {
caching: Env.boolean("ENABLE_CACHE").default(true),
rateLimit: Env.number("RATE_LIMIT").default(100),
},
api: {
version: Env.string("API_VERSION").default("v1"),
prefix: Env.string("API_PREFIX").default("/api"),
},
});
Environment Field Builders
The env object provides type-safe builders for environment variables:
String Values
Env.string("VAR_NAME")
.required() // Make required
.default("fallback") // Set default value
.enum(["a", "b", "c"]) // Restrict to specific values
.pattern(/^[a-z]+$/) // Validate with regex
Number Values
Env.number("VAR_NAME")
.required()
.default(100)
.min(1) // Minimum value
.max(1000) // Maximum value
.integer() // Must be integer
Boolean Values
Env.boolean("VAR_NAME")
.required()
.default(false)
// Accepts: true, false, "true", "false", "1", "0", "yes", "no"
Array Values
Env.array("VAR_NAME")
.required()
.default(["item1", "item2"])
.separator(",") // Custom separator (default: ",")
// VAR_NAME=item1,item2,item3 → ["item1", "item2", "item3"]
Object Values
Env.json("VAR_NAME")
.required()
.default({ key: "value" })
// VAR_NAME='{"key":"value"}' → { key: "value" }
URL Values
Env.url("VAR_NAME", {
protocols: ["http", "https"],
noTrailingSlash: true,
development: "http://localhost:4000",
production: "https://api.example.com",
description: "External service base URL",
})
Port Values
Env.port("PORT", {
default: 3000,
description: "HTTP server port",
})
// Range-validated (1-65535) and type-cast to number.
Enum Values
Env.enum("LOG_LEVEL", ["debug", "info", "warn", "error"], {
development: "debug",
production: "warn",
})
// Inferred as "debug" | "info" | "warn" | "error".
Secret Values
Env.secret("JWT_SECRET", {
minLength: 32,
description: "Secret key for signing JWTs",
hint: "Generate with: openssl rand -base64 32",
})
// Returns a SecretValue, automatically redacted in logs.
See SecretValue Features for the full API surface.
Using Configuration
In Services
Inject configuration using the Config provider:
import { provide, inject, Config } from "@expressots/core";
@provide(DatabaseService)
export class DatabaseService {
constructor(@inject(Config) private config: Config) {}
connect() {
const dbUrl = this.config.get("database.url");
const poolSize = this.config.get("database.pool");
// TypeScript knows the types!
return this.createPool(dbUrl, poolSize);
}
}
In Application Class
Access configuration in the application class:
import { AppExpress } from "@expressots/adapter-express";
export class App extends AppExpress {
async globalConfiguration(): Promise<void> {
const config = this.getConfig();
this.setGlobalRoutePrefix(config.get("api.prefix"));
}
async configureServices(): Promise<void> {
const config = this.getConfig();
if (config.get("features.caching")) {
this.Middleware.addCaching();
}
if (config.get("features.rateLimit") > 0) {
this.Middleware.addRateLimiter({
max: config.get("features.rateLimit"),
});
}
}
}
Helpful Validation
When configuration is invalid, ExpressoTS provides detailed error messages:
❌ Configuration Error: DATABASE_URL
Missing required environment variable
Expected: A valid database connection string
Example: postgresql://user:password@localhost:5432/mydb
Hint: Create a .env file with DATABASE_URL=your_connection_string
Validation Examples
// Required field missing
Env.string("API_KEY").required()
// ❌ Missing required environment variable: API_KEY
// Invalid number
Env.number("PORT").min(1).max(65535)
// PORT=99999
// ❌ PORT must be between 1 and 65535, got: 99999
// Invalid enum
Env.string("LOG_LEVEL").enum(["debug", "info", "warn", "error"])
// LOG_LEVEL=verbose
// ❌ LOG_LEVEL must be one of: debug, info, warn, error, got: verbose
// Pattern mismatch
Env.string("SLUG").pattern(/^[a-z0-9-]+$/)
// SLUG=My Slug
// ❌ SLUG does not match pattern: /^[a-z0-9-]+$/
Environment-Specific Configuration
Define different configurations per environment:
import { defineConfig, Env, when } from "@expressots/core";
export default defineConfig({
database: {
url: when(Env.string("NODE_ENV"), {
development: "postgresql://localhost:5432/dev",
test: "postgresql://localhost:5432/test",
production: Env.string("DATABASE_URL").required(),
}),
pool: when(Env.string("NODE_ENV"), {
development: 5,
test: 2,
production: 20,
}),
},
logging: {
level: when(Env.string("NODE_ENV"), {
development: "debug",
test: "warn",
production: "error",
}),
},
});
Inline multi-environment defaults
Every Env.* builder also accepts per-environment defaults directly in its options object. This is the most concise way to express "use one default in dev, another in production":
import { defineConfig, Env } from "@expressots/core";
export const config = defineConfig({
server: {
port: Env.port("PORT", { default: 3000 }),
host: Env.string("HOST", {
development: "localhost",
staging: "0.0.0.0",
production: "0.0.0.0",
description: "Server bind address",
}),
},
database: {
url: Env.url("DATABASE_URL", {
development: "postgresql://localhost:5432/dev_db",
staging: undefined, // Must be set explicitly
production: undefined, // Must be set explicitly
description: "PostgreSQL connection URL",
hint: "Format: postgresql://user:password@host:port/database",
}),
poolSize: Env.number("DB_POOL_SIZE", {
development: 5,
staging: 20,
production: 50,
min: 1,
max: 100,
}),
},
auth: {
jwtSecret: Env.secret("JWT_SECRET", {
minLength: 32,
description: "Secret key for signing JWTs",
hint: "Generate with: openssl rand -base64 32",
}),
},
features: {
allowedOrigins: Env.array("ALLOWED_ORIGINS", {
default: ["http://localhost:3000"],
delimiter: ",",
description: "Allowed CORS origins (comma-separated)",
}),
enableRateLimit: Env.boolean("ENABLE_RATE_LIMIT", {
development: false,
staging: true,
production: true,
}),
},
});
export type AppConfig = typeof config.values;
The resolved environment is picked from NODE_ENV. Passing undefined for a given environment means "no default, must be provided explicitly", which makes the field required for that environment.
Alternative: Multiple Config Files
// config/development.ts
export default defineConfig({
database: { url: "postgresql://localhost:5432/dev" },
logging: { level: "debug" },
});
// config/production.ts
export default defineConfig({
database: { url: Env.string("DATABASE_URL").required() },
logging: { level: "error" },
});
// config/index.ts
import development from "./development";
import production from "./production";
export default Env.string("NODE_ENV").value === "production"
? production
: development;
Secret Management
Use SecretValue for sensitive data:
import { defineConfig, Env, SecretValue } from "@expressots/core";
export default defineConfig({
auth: {
jwtSecret: new SecretValue(Env.string("JWT_SECRET").required()),
apiKey: new SecretValue(Env.string("API_KEY").required()),
},
});
SecretValue Features
const secret = config.get("auth.jwtSecret");
// Access the actual value
const value = secret.reveal(); // Returns the actual secret
// Safe logging (won't expose secret)
console.log(secret); // Outputs: [SECRET]
console.log(secret.toString()); // Outputs: [SECRET]
console.log(secret.toJSON()); // Outputs: "[REDACTED]"
// Comparison without exposing
secret.equals("expected-value"); // Returns true/false
Configuration in expressots.config.ts
You can also use configuration in expressots.config.ts:
// expressots.config.ts
import { ExpressoTSConfig } from "@expressots/shared";
const config: ExpressoTSConfig = {
sourceRoot: "src",
entryPoint: {
opinionated: "main.ts",
nonOpinionated: "main.ts",
},
env: {
development: ".env.development",
production: ".env.production",
test: ".env.test",
},
};
export default config;
Configuration Schema
Define a schema for configuration validation:
import { defineConfig, defineSchema, Env } from "@expressots/core";
const schema = defineSchema({
database: {
url: {
type: "string",
required: true,
description: "Database connection URL",
example: "postgresql://user:pass@localhost:5432/db",
},
pool: {
type: "number",
default: 10,
min: 1,
max: 100,
description: "Connection pool size",
},
},
});
export default defineConfig(schema, {
database: {
url: Env.string("DATABASE_URL"),
pool: Env.number("DB_POOL_SIZE"),
},
});
Best Practices
- Use Type-Safe Builders: Always use
Env.string(),Env.number(), etc. - Provide Defaults: Use
.default()for optional configuration - Mark Required Fields: Use
.required()for essential config - Use SecretValue: Wrap sensitive data in
SecretValue - Validate Early: Configuration is validated at startup
- Document Config: Use schema descriptions for documentation
Migration from v3
If you're using the v3 Env provider, you can continue using it:
// v3 style (still works in v4)
import { Env } from "@expressots/core";
this.initEnvironment("development", {
env: {
development: ".env.development",
production: ".env.production",
},
});
// Access via Env provider
const port = this.Provider.get(Env).get("PORT");
The new defineConfig approach is recommended for new projects but is fully optional.
Comparison with Other Frameworks
| Feature | ExpressoTS | NestJS | Spring Boot |
|---|---|---|---|
| Type-Safe Config | ✅ Full inference | ⚠️ Partial | ⚠️ Via annotations |
| Helpful Validation | ✅ Examples & hints | ❌ Basic errors | ⚠️ Basic errors |
| Environment Switching | ✅ Automatic | ⚠️ Manual | ✅ Profiles |
| Secret Management | ✅ SecretValue | ❌ Manual | ⚠️ Via Vault |
| Schema Definition | ✅ Built-in | ❌ Via class-validator | ✅ Via annotations |
Support the Project
ExpressoTS is MIT-licensed open source. See the support guide to contribute.