| id |
schema-json-file-multiple-files |
| title |
Validating Multiple Config Files |
| category |
json-validation |
| skillLevel |
intermediate |
| tags |
schema |
json |
file |
multiple |
parallel |
aggregation |
|
| lessonOrder |
37 |
| rule |
| description |
Validate Multiple Config Files using Schema. |
|
| summary |
Complex applications often need to load multiple configuration files: database config, API keys, logging settings, feature flags, etc. Loading them one-by-one is slow, and you need to handle both... |
Complex applications often need to load multiple configuration files: database config, API keys, logging settings, feature flags, etc. Loading them one-by-one is slow, and you need to handle both complete failures (stop if any critical config is missing) and partial failures (some configs optional, others required). You need parallel validation with clear reporting of which configs succeeded and which failed.
import { Schema, Effect } from "effect";
import { FileSystem } from "@effect/platform";
import { NodeFileSystem } from "@effect/platform-node";
// 1. Define schemas for different config types
const DatabaseConfig = Schema.Struct({
host: Schema.String.pipe(Schema.minLength(1)),
port: Schema.Number.pipe(Schema.int(), Schema.between(1, 65535)),
username: Schema.String,
password: Schema.String,
database: Schema.String,
});
type DatabaseConfig = typeof DatabaseConfig.Type;
const ApiConfig = Schema.Struct({
baseUrl: Schema.String.pipe(Schema.minLength(1)),
apiKey: Schema.String.pipe(Schema.minLength(10)),
timeout: Schema.Number.pipe(
Schema.int(),
Schema.positive(),
Schema.optionalWith({ default: () => 5000 })
),
});
type ApiConfig = typeof ApiConfig.Type;
const FeatureFlags = Schema.Struct({
enableNewUI: Schema.Boolean.pipe(
Schema.optionalWith({ default: () => false })
),
enableAnalytics: Schema.Boolean.pipe(
Schema.optionalWith({ default: () => true })
),
maintenanceMode: Schema.Boolean.pipe(
Schema.optionalWith({ default: () => false })
),
});
type FeatureFlags = typeof FeatureFlags.Type;
// 2. Generic file loader function
const loadJsonFile = <A extends Schema.Schema.Any>(
schema: A,
filePath: string
) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const content = yield* fs.readFileString(filePath);
let jsonData: unknown = JSON.parse(content);
const result = yield* Schema.decodeUnknown(schema)(jsonData);
return result;
});
// 3. Load multiple configs with parallel execution
const loadAllConfigs = (configDir: string) =>
Effect.gen(function* () {
// Load all three configs in parallel
const [database, api, features] = yield* Effect.all(
[
loadJsonFile(DatabaseConfig, `${configDir}/database.json`),
loadJsonFile(ApiConfig, `${configDir}/api.json`),
loadJsonFile(FeatureFlags, `${configDir}/features.json`),
],
{ concurrency: 3 }
);
return { database, api, features };
});
// 4. Load configs with error recovery (some optional)
const loadConfigsWithFallback = (configDir: string) =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
// Database config is required
const database = yield* loadJsonFile(
DatabaseConfig,
`${configDir}/database.json`
).pipe(
Effect.mapError((error) => ({
_tag: "DatabaseConfigError" as const,
message: `Database config invalid: ${error.message}`,
}))
);
// API config is required
const api = yield* loadJsonFile(
ApiConfig,
`${configDir}/api.json`
).pipe(
Effect.mapError((error) => ({
_tag: "ApiConfigError" as const,
message: `API config invalid: ${error.message}`,
}))
);
// Feature flags are optional - use defaults if missing
const features = yield* loadJsonFile(
FeatureFlags,
`${configDir}/features.json`
).pipe(
Effect.catchAll(() =>
Effect.succeed({
enableNewUI: false,
enableAnalytics: true,
maintenanceMode: false,
})
)
);
return { database, api, features };
});
// 5. Validate and display results
const initializeWithConfigs = (configDir: string) =>
Effect.gen(function* () {
console.log(`📂 Loading configs from ${configDir}...`);
const { database, api, features } = yield* loadConfigsWithFallback(
configDir
);
console.log("✅ All configs loaded successfully!");
console.log("\n📊 Configuration Summary:");
console.log("\n🗄️ Database:");
console.log(` Host: ${database.host}:${database.port}`);
console.log(` Database: ${database.database}`);
console.log("\n🌐 API:");
console.log(` Base URL: ${api.baseUrl}`);
console.log(` Timeout: ${api.timeout}ms`);
console.log("\n🚩 Features:");
console.log(
` New UI: ${features.enableNewUI ? "✓" : "✗"}`
);
console.log(
` Analytics: ${features.enableAnalytics ? "✓" : "✗"}`
);
console.log(
` Maintenance: ${features.maintenanceMode ? "✓" : "✗"}`
);
return { database, api, features };
});
// 6. Usage: Load and validate all configs
Effect.runPromise(
initializeWithConfigs("./config").pipe(
Effect.provideLayer(NodeFileSystem.layer)
)
)
.then((config) => {
console.log("\n🚀 Application ready to start");
})
.catch((error) => {
console.error(
`\n❌ ${error._tag}: ${error.message}`
);
process.exit(1);
});
// Example file structure:
// config/
// ├── database.json
// │ {
// │ "host": "postgres.example.com",
// │ "port": 5432,
// │ "username": "appuser",
// │ "password": "secret",
// │ "database": "myapp_prod"
// │ }
// ├── api.json
// │ {
// │ "baseUrl": "https://api.example.com",
// │ "apiKey": "sk_live_123456789",
// │ "timeout": 8000
// │ }
// └── features.json (optional, uses defaults if missing)
// {
// "enableNewUI": true,
// "enableAnalytics": true,
// "maintenanceMode": false
// }
| Concept |
Explanation |
Effect.all([...], { concurrency: 3 }) |
Load configs in parallel for speed |
Generic loadJsonFile function |
Reusable for different schema types |
Effect.catchAll for optional files |
Gracefully fall back if feature flags missing |
| Typed error tags |
Know exactly which config failed |
| Required vs optional separation |
Database/API required, features optional |
| Parallel failure handling |
If any required config fails, whole Effect fails |
| Type safety across multiple files |
Each config fully typed independently |
- Multi-service applications with separate config files
- Microservices that need database, API, and cache configs
- Loading feature flags alongside main configuration
- Applications with optional vs required configuration files
- Parallel loading for startup performance
- Complex applications where configs are modular
- Handling both config failures and missing optional configs
- Reporting which specific config file failed to load