Configuration
Type-safe configuration management with Zod validation
The @justscale/config package provides type-safe configuration management with Zod schema validation, environment variable support, profile management, and runtime mutations.
Installation
pnpm add @justscale/configDefining Config Partials
Use defineConfigPartial to create type-safe configuration sections with Zod schemas:
import { defineConfigPartial } from '@justscale/config';
import { z } from 'zod';
// Define a config partial with a Zod schema
export const DatabaseConfig = defineConfigPartial('database', z.object({
host: z.string().default('localhost'),
port: z.number().default(5432),
database: z.string(),
username: z.string(),
password: z.string(),
poolSize: z.number().min(1).max(100).default(10),
}));
// Type is inferred from the schema
type DatabaseConfigType = z.infer<typeof DatabaseConfig.schema>;
// { host: string; port: number; database: string; ... }Injecting Configuration
Use Config.of() to create injection tokens for config partials:
import { createService } from '@justscale/core';
import { Config } from '@justscale/config';
import { DatabaseConfig } from '../config/database';
export const DatabaseService = createService({
inject: {
// Config.of() creates a type-safe injection token
config: Config.of(DatabaseConfig),
},
factory: ({ config }) => {
// config is fully typed based on the Zod schema
const connectionString = `postgres://${config.username}:${config.password}@${config.host}:${config.port}/${config.database}`;
return {
getConnectionString: () => connectionString,
getPoolSize: () => config.poolSize,
};
},
});Providing Configuration
Use createConfig to provide config values at application startup:
import { createClusterBuilder } from '@justscale/core';
import { AppConfig } from './config';
import { DatabaseService } from './services/database.service';
const cluster = createClusterBuilder()
.add(AppConfig) // Provide configuration
.add(DatabaseService) // Services can now inject config
.build();
await cluster.compile();
await cluster.start();Environment Variables
The EnvServiceDef provides type-safe access to environment variables:
import { createService } from '@justscale/core';
import { EnvServiceDef } from '@justscale/config';
export const ExampleService = createService({
inject: {
env: EnvServiceDef,
},
factory: ({ env }) => ({
getApiKey: () => env.get('API_KEY'),
getOptionalValue: () => env.get('OPTIONAL_VAR', 'default-value'),
isProduction: () => env.get('NODE_ENV') === 'production',
}),
});Profile-Based Configuration
Use profiles to manage different configuration sets for development, staging, and production environments:
import { createConfig } from '@justscale/config';
import { DatabaseConfig } from './database';
// Development profile
export const DevConfig = createConfig({
factory: () => ({
[DatabaseConfig.key]: {
host: 'localhost',
port: 5432,
database: 'myapp_dev',
username: 'dev',
password: 'dev',
poolSize: 5,
},
}),
});
// Production profile
export const ProdConfig = createConfig({
factory: () => ({
[DatabaseConfig.key]: {
host: process.env.DB_HOST!,
port: parseInt(process.env.DB_PORT!),
database: process.env.DB_NAME!,
username: process.env.DB_USER!,
password: process.env.DB_PASS!,
poolSize: 50,
},
}),
});Runtime Mutations
The ConfigServiceDef allows runtime configuration updates and watching for changes:
import { createService } from '@justscale/core';
import { ConfigServiceDef, Config } from '@justscale/config';
import { AppSettings } from '../config/settings';
export const SettingsService = createService({
inject: {
configService: ConfigServiceDef,
settings: Config.of(AppSettings),
},
factory: ({ configService, settings }) => ({
// Get current value
getDebugMode: () => settings.debugMode,
// Update config at runtime
setDebugMode: async (enabled: boolean) => {
configService.set(AppSettings, 'debugMode', enabled);
},
// Watch for config changes
watchSettings: async function* () {
for await (const [oldConfig, newConfig] of configService.watch(AppSettings)) {
yield { old: oldConfig, new: newConfig };
}
},
}),
});Runtime mutations are persisted to .justscale/config.json and survive application restarts.
CLI Integration
Add configuration CLI commands to manage settings from the command line:
# View all config
justscale config list
# Get a specific value
justscale config get database.host
# Set a value at runtime
justscale config set database.poolSize 25
# View config as JSON
justscale config dumpProfile Service
The ProfileServiceDef provides file-based profile management for switching between configuration sets (local, dev, staging, production):
import { createService } from '@justscale/core';
import { ProfileServiceDef } from '@justscale/config';
export const DeployService = createService({
inject: {
profiles: ProfileServiceDef,
},
factory: ({ profiles }) => ({
// Get active profile (checks JUSTSCALE_PROFILE env, then .justscale/.active-profile)
getCurrentProfile: () => profiles.active(),
// Switch to a different profile
switchProfile: (name: string) => {
profiles.use(name); // Throws if profile doesn't exist
},
// List all available profiles
listProfiles: () => profiles.list(),
// Create a new profile (optionally copy from existing)
createProfile: (name: string, copyFrom?: string) => {
profiles.create(name, copyFrom);
},
// Compare two profiles
diffProfiles: (from: string, to: string) => {
return profiles.diff(from, to);
// Returns: [{ key: 'database.host', from: 'localhost', to: 'prod-db' }, ...]
},
}),
});Profile Priority
The active profile is determined by (in order):
JUSTSCALE_PROFILEenvironment variable.justscale/.active-profilefile- Default:
local
Profile CLI Commands
# List available profiles
justscale profile list
# Switch to a profile
justscale profile use staging
# Create a new profile (copy from existing)
justscale profile create prod --from staging
# Compare profiles
justscale profile diff staging prodValidation
All configuration is validated against the Zod schema at startup and during runtime mutations:
import { defineConfigPartial } from '@justscale/config';
import { z } from 'zod';
export const ApiConfig = defineConfigPartial('api', z.object({
// Required fields
baseUrl: z.string().url(),
apiKey: z.string().min(32),
// Optional with defaults
timeout: z.number().positive().default(30000),
retries: z.number().int().min(0).max(10).default(3),
// Enum values
logLevel: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
}));If validation fails, an error is thrown with detailed information about which fields failed and why.
Best Practices
- Group related settings - Create separate config partials for each concern (database, redis, api, etc.)
- Use sensible defaults - Provide default values in your Zod schemas for development convenience
- Validate early - Configuration is validated at startup, failing fast if values are invalid
- Keep secrets out of code - Use environment variables for sensitive values like passwords and API keys
- Use profiles - Create separate config components for different environments to avoid conditional logic