Skip to content

Latest commit

 

History

History
246 lines (211 loc) · 7.29 KB

File metadata and controls

246 lines (211 loc) · 7.29 KB
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...

Problem

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.

Solution

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
//     }

Why This Works

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

When to Use

  • 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

Related Patterns