AIpto is a robust service configuration management system designed for distributed architectures. It provides a complete solution for managing service configurations with validation, type safety, and a flexible API.
- Features
- Architecture
- Project Structure
- Installation
- Usage Examples
- Technical Implementation
- Data Flow
- Components
- Use Cases
- Best Practices
- Performance Considerations
- Integration Scenarios
- Frequently Asked Questions
- Troubleshooting
- Comparison with Other Solutions
- Roadmap
- API Documentation
- Development
- Contributing
- License
- Type-safe Configuration Access: Get configuration values with proper TypeScript typing
- Dot Notation Path Access: Access nested configuration values using simple dot notation
- Schema Validation: Validate configurations against JSON schemas
- Environment Support: Load different configurations based on environment
- Deep Merging: Intelligently merge configuration objects
- Change Detection: Track and validate configuration changes
- Modular Design: Easily extensible architecture
- Lightweight Footprint: Minimal dependencies and small bundle size
- Browser Compatible: Works in both Node.js and browser environments
- Immutable Options: Optional immutable configurations to prevent unintended changes
- Runtime Updates: Change configuration on the fly with validation
The AIpto configuration system is designed with modularity and extensibility in mind:
The system consists of several core components:
- Configuration Manager: The central component that manages configuration data
- Type Definitions: TypeScript types for type safety
- Utility Functions: Helper functions for manipulating configuration objects
- Schema Validation: JSON schema validation using Ajv
The architecture follows key design principles:
- Separation of Concerns: Each component has a single responsibility
- Dependency Injection: Components are loosely coupled
- Immutability: Configuration objects are not modified directly
- Type Safety: TypeScript is used throughout to ensure type safety
- Extensibility: The system can be extended with custom validators and processors
The AIpto project is organized with the following directory structure:
aipto/
├── assets/ # Static assets
│ └── images/ # Images and diagrams
├── dist/ # Compiled output (generated)
├── docs/ # Documentation files
│ ├── API.md # API documentation
│ └── DEVELOPMENT.md # Development guide
├── src/ # Source code
│ ├── config/ # Configuration management core
│ │ ├── types.ts # Type definitions
│ │ └── config-manager.ts # Core implementation
│ ├── utils/ # Utility functions
│ │ └── object-utils.ts # Object manipulation utilities
│ ├── examples/ # Usage examples
│ └── index.ts # Main entry point
├── .eslintrc.json # ESLint configuration
├── .gitignore # Git ignore rules
├── jest.config.js # Jest configuration
├── LICENSE # MIT license
├── package.json # Project metadata
├── README.md # Project readme
└── tsconfig.json # TypeScript configuration
npm install @aipto/config
yarn add @aipto/config
<script src="https://cdn.jsdelivr.net/npm/@aipto/config/dist/aipto-config.min.js"></script>
import { createConfig } from '@aipto/config';
// Create a configuration
const config = createConfig({
server: {
port: 3000,
host: 'localhost'
},
database: {
url: 'mongodb://localhost:27017',
name: 'myapp'
}
});
// Access configuration values
const serverPort = config.get<number>('server.port');
console.log(`Server starting on port ${serverPort}`);
// Modify configuration
config.set('server.port', 8080);
// Validate configuration
const validationResult = config.validate();
if (!validationResult.valid) {
console.error('Configuration validation failed:', validationResult.errors);
}
import { createConfig } from '@aipto/config';
// Define a schema for validation
const schema = {
type: 'object',
required: ['server'],
properties: {
server: {
type: 'object',
required: ['port'],
properties: {
port: { type: 'number', minimum: 1024, maximum: 65535 }
}
}
}
};
// Create configuration with schema
const config = createConfig(
{
server: { port: 3000 }
},
{ schema }
);
// Configuration will be validated against the schema
const result = config.validate();
console.log(`Is config valid? ${result.valid}`);
import { createConfig, ConfigObject } from '@aipto/config';
// Base configuration
const baseConfig = {
server: {
port: 3000,
host: 'localhost'
}
};
// Environment-specific configuration
const envConfigs = {
development: {
logging: { level: 'debug' }
},
production: {
server: { port: 8080 },
logging: { level: 'error' }
}
};
// Get environment
const env = process.env.NODE_ENV || 'development';
// Create and merge configurations
const config = createConfig(baseConfig);
config.update(envConfigs[env] || {});
console.log(`Running in ${env} mode on port ${config.get('server.port')}`);
import { createConfig, ConfigObject } from '@aipto/config';
import * as fs from 'fs';
// Load configuration from a file
function loadConfigFromFile(filePath: string): ConfigObject {
try {
const fileContent = fs.readFileSync(filePath, 'utf-8');
return JSON.parse(fileContent);
} catch (error) {
console.error(`Error loading config from ${filePath}:`, error);
return {};
}
}
// Load configuration from environment variables
function loadConfigFromEnv(prefix: string = 'APP_'): ConfigObject {
const config: ConfigObject = {};
Object.keys(process.env).forEach(key => {
if (key.startsWith(prefix)) {
const configKey = key.substring(prefix.length).toLowerCase().replace(/_/g, '.');
let value: any = process.env[key];
// Convert boolean strings
if (value === 'true') value = true;
if (value === 'false') value = false;
// Convert number strings
if (/^-?\d+(\.\d+)?$/.test(value)) {
value = Number(value);
}
// Set nested value
setNestedValue(config, configKey, value);
}
});
return config;
}
// Create a configuration system with multiple sources
const baseConfig = loadConfigFromFile('./config/default.json');
const envConfig = loadConfigFromFile(`./config/${process.env.NODE_ENV || 'development'}.json`);
const envVarConfig = loadConfigFromEnv();
const config = createConfig(baseConfig);
config.update(envConfig);
config.update(envVarConfig);
// Validate the final configuration
const validationResult = config.validate();
if (!validationResult.valid) {
console.error('Configuration validation failed:', validationResult.errors);
process.exit(1);
}
export default config;
The system is built around these core components:
// Core configuration value types
export type ConfigValue = string | number | boolean | null | ConfigObject | ConfigArray;
export interface ConfigObject { [key: string]: ConfigValue; }
export type ConfigArray = ConfigValue[];
// Validation result types
export interface ValidationResult {
valid: boolean;
errors?: ValidationError[];
}
// Configuration interface
export interface Config {
get<T = ConfigValue>(path: string): T;
set(path: string, value: ConfigValue): void;
getAll(): ConfigObject;
update(config: ConfigObject): void; // Update configuration with new values
validate(): ValidationResult;
}
The ConfigManager
class implements the Config
interface and provides the core functionality:
export class ConfigManager implements Config {
private config: ConfigObject;
private schema?: object;
private ajv: Ajv;
constructor(options: ConfigOptions = {}) {
this.config = options.defaultValues || {};
this.schema = options.schema;
this.ajv = new Ajv({ allErrors: true });
}
// Get a configuration value
public get<T = ConfigValue>(path: string): T {
// Implementation...
}
// Set a configuration value
public set(path: string, value: ConfigValue): void {
// Implementation...
}
// Get all configuration
public getAll(): ConfigObject {
// Implementation...
}
// Update with new values
public update(config: ConfigObject): void {
// Implementation...
}
// Validate configuration
public validate(): ValidationResult {
// Implementation...
}
}
Helper functions for configuration object manipulation:
// Get a nested value using dot notation
export function getNestedValue(obj: ConfigObject, path: string): ConfigValue | undefined {
const parts = path.split('.');
let current: any = obj;
for (const part of parts) {
if (current === undefined || current === null) {
return undefined;
}
current = current[part];
}
return current;
}
// Deep merge two objects
export function deepMerge(target: ConfigObject, source: ConfigObject): ConfigObject {
// Implementation details...
}
The data flow within the configuration system follows this pattern:
- Configuration Loading: Initial configuration is loaded from various sources (defaults, files, environment variables)
- Configuration Processing: The configuration manager processes and stores the configuration
- Value Access: Application code accesses configuration values using the
get
method - Validation: Configuration is validated against a schema when
validate
is called - Updates: Configuration can be updated at runtime with new values
The AIpto system is composed of several components that work together:
- Configuration Manager: The central component that manages configuration data and implements the core API
- Type Definitions: Provides TypeScript types for configuration values and interfaces
- Utility Functions: Contains helper functions for object manipulation and configuration operations
- Schema Validation: Uses the Ajv library to validate configurations against JSON schemas
- Public API: Exposes the public interfaces and factory functions like
createConfig
AIpto has minimal external dependencies:
- Ajv: Used for JSON Schema validation
- lodash: Used for some utility functions (optional)
AIpto can be used in a variety of scenarios:
In a microservices architecture, each service requires its own configuration. AIpto provides a consistent way to manage configurations across all services:
// service-a/config.ts
import { createConfig } from '@aipto/config';
export default createConfig({
name: 'service-a',
port: 3001,
dependencies: ['service-b', 'service-c'],
database: {
url: process.env.DB_URL || 'mongodb://localhost:27017',
name: 'service-a'
}
});
// service-b/config.ts
import { createConfig } from '@aipto/config';
export default createConfig({
name: 'service-b',
port: 3002,
dependencies: ['service-d'],
database: {
url: process.env.DB_URL || 'mongodb://localhost:27017',
name: 'service-b'
}
});
AIpto can be used to implement feature flags for enabling or disabling features:
import { createConfig } from '@aipto/config';
const config = createConfig({
features: {
newUserInterface: process.env.ENABLE_NEW_UI === 'true',
betaFeatures: process.env.ENABLE_BETA === 'true',
analytics: true
}
});
// In your application
if (config.get('features.newUserInterface')) {
// Render new UI
} else {
// Render old UI
}
For long-running applications, configurations can be updated without restarting:
import { createConfig } from '@aipto/config';
import { watchFile } from 'fs';
const config = createConfig(require('./config.json'));
// Watch for changes to the config file
watchFile('./config.json', () => {
try {
// Clear require cache
delete require.cache[require.resolve('./config.json')];
// Load new configuration
const newConfig = require('./config.json');
// Update configuration
config.update(newConfig);
console.log('Configuration updated');
} catch (error) {
console.error('Error updating configuration:', error);
}
});
For applications serving multiple tenants, AIpto can manage tenant-specific configurations:
import { createConfig, ConfigObject } from '@aipto/config';
// Load base configuration
const baseConfig = {
application: {
name: 'Multi-tenant App',
version: '1.0.0'
},
defaults: {
theme: 'light',
language: 'en'
}
};
// Tenant-specific configurations
const tenantConfigs: Record<string, ConfigObject> = {
'tenant-a': {
branding: {
logo: 'https://tenant-a.com/logo.png',
colors: {
primary: '#ff0000',
secondary: '#00ff00'
}
},
defaults: {
language: 'fr'
}
},
'tenant-b': {
branding: {
logo: 'https://tenant-b.com/logo.png',
colors: {
primary: '#0000ff',
secondary: '#ffff00'
}
}
}
};
// Function to get configuration for a specific tenant
function getTenantConfig(tenantId: string) {
const config = createConfig(baseConfig);
if (tenantConfigs[tenantId]) {
config.update(tenantConfigs[tenantId]);
}
return config;
}
// Usage
const tenantAConfig = getTenantConfig('tenant-a');
const tenantBConfig = getTenantConfig('tenant-b');
console.log(tenantAConfig.get('defaults.language')); // 'fr'
console.log(tenantBConfig.get('defaults.language')); // 'en'
Organize your configurations logically to make them easier to manage:
const config = createConfig({
// Server-related configurations
server: {
port: 3000,
host: 'localhost',
timeout: 30000
},
// Database-related configurations
database: {
url: 'mongodb://localhost:27017',
name: 'myapp',
pool: {
min: 5,
max: 10
}
},
// Application-specific settings
application: {
name: 'MyApp',
version: '1.0.0',
environment: 'development'
},
// Feature flags
features: {
enableNewUI: false,
enableAnalytics: true
}
});
Establish a clear hierarchy for configuration sources:
- Default configurations: Hardcoded defaults
- Configuration files: Environment-specific files
- Environment variables: Override specific values
- Command line arguments: Highest priority overrides
Example implementation:
import { createConfig } from '@aipto/config';
import defaultConfig from './config/defaults';
import devConfig from './config/development';
import prodConfig from './config/production';
import { loadFromEnv } from './config/env-loader';
import { loadFromArgs } from './config/args-loader';
const env = process.env.NODE_ENV || 'development';
const envConfig = env === 'production' ? prodConfig : devConfig;
const config = createConfig(defaultConfig);
config.update(envConfig);
config.update(loadFromEnv());
config.update(loadFromArgs());
export default config;
Always define schemas for your configurations to catch errors early:
const serverConfigSchema = {
type: 'object',
required: ['port', 'host'],
properties: {
port: {
type: 'number',
minimum: 1024,
maximum: 65535,
description: 'Port number for the server to listen on'
},
host: {
type: 'string',
description: 'Hostname or IP address to bind to'
},
timeout: {
type: 'number',
minimum: 1000,
description: 'Request timeout in milliseconds'
}
}
};
const config = createConfig(
{ server: { port: 3000, host: 'localhost' } },
{ schema: serverConfigSchema }
);
Never store sensitive information directly in your configuration:
// BAD:
const config = createConfig({
database: {
password: 'super-secret-password' // Don't do this!
}
});
// GOOD:
const config = createConfig({
database: {
password: process.env.DB_PASSWORD
}
});
For frequently accessed configuration values, consider caching them:
import { createConfig } from '@aipto/config';
const config = createConfig({ /* configuration */ });
// Cache for frequently accessed values
const configCache = new Map<string, any>();
function getCachedConfig<T>(path: string): T {
if (!configCache.has(path)) {
configCache.set(path, config.get<T>(path));
}
return configCache.get(path);
}
// Clear cache when configuration is updated
function updateConfig(newConfig: any) {
config.update(newConfig);
configCache.clear(); // Invalidate cache
}
For large configurations, consider lazy loading sections:
import { createConfig, ConfigObject } from '@aipto/config';
import * as fs from 'fs/promises';
// Main configuration
const mainConfig = createConfig({ /* core configuration */ });
// Lazy load additional configurations
const lazyConfigs: Record<string, Promise<ConfigObject>> = {};
async function loadConfigSection(section: string): Promise<ConfigObject> {
if (!lazyConfigs[section]) {
lazyConfigs[section] = fs.readFile(`./config/${section}.json`, 'utf-8')
.then(data => JSON.parse(data))
.catch(error => {
console.error(`Error loading ${section} configuration:`, error);
return {};
});
}
return lazyConfigs[section];
}
// Usage
async function initializeModule(section: string) {
const sectionConfig = await loadConfigSection(section);
mainConfig.update({ [section]: sectionConfig });
return mainConfig;
}
For browser applications, consider tree-shaking to reduce bundle size:
// Instead of importing everything
// import * as AIpto from '@aipto/config';
// Only import what you need
import { createConfig } from '@aipto/config';
import { getNestedValue } from '@aipto/config/utils';
import express from 'express';
import { createConfig } from '@aipto/config';
const config = createConfig({
server: {
port: 3000,
cors: {
enabled: true,
origins: ['http://localhost:8080']
}
},
rateLimiter: {
enabled: true,
maxRequests: 100,
windowMs: 60000
}
});
const app = express();
// Configure middleware based on configuration
if (config.get('server.cors.enabled')) {
const cors = require('cors');
app.use(cors({
origin: config.get('server.cors.origins')
}));
}
if (config.get('rateLimiter.enabled')) {
const rateLimit = require('express-rate-limit');
app.use(rateLimit({
windowMs: config.get('rateLimiter.windowMs'),
max: config.get('rateLimiter.maxRequests')
}));
}
// Start server
const port = config.get<number>('server.port');
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
import React, { createContext, useContext } from 'react';
import { createConfig, Config } from '@aipto/config';
// Create configuration
const appConfig = createConfig({
theme: {
primary: '#0070f3',<
8000
/span>
secondary: '#ff4081',
dark: false
},
api: {
baseUrl: process.env.REACT_APP_API_URL || 'http://localhost:3000/api'
},
features: {
comments: true,
sharing: true
}
});
// Create context
const ConfigContext = createContext<Config>(appConfig);
// Provider component
export const ConfigProvider: React.FC<{children: React.ReactNode}> = ({ children }) => {
return (
<ConfigContext.Provider value={appConfig}>
{children}
</ConfigContext.Provider>
);
};
// Hook for accessing configuration
export const useConfig = () => useContext(ConfigContext);
// Usage in components
const ThemeButton = () => {
const config = useConfig();
const primary = config.get<string>('theme.primary');
return (
<button style={{ backgroundColor: primary }}>
Themed Button
</button>
);
};
// lib/config.ts
import { createConfig } from '@aipto/config';
const config = createConfig({
site: {
title: 'My Next.js Site',
description: 'A site built with Next.js and AIpto'
},
api: {
baseUrl: process.env.NEXT_PUBLIC_API_URL || '/api'
},
features: {
comments: process.env.NEXT_PUBLIC_FEATURE_COMMENTS === 'true'
}
});
export default config;
// pages/_app.tsx
import type { AppProps } from 'next/app';
import config from '../lib/config';
function MyApp({ Component, pageProps }: AppProps) {
// Inject config into pageProps
const enhancedPageProps = {
...pageProps,
config: config.getAll()
};
return <Component {...enhancedPageProps} />;
}
export default MyApp;
// pages/index.tsx
import { GetServerSideProps } from 'next';
import config from '../lib/config';
export const getServerSideProps: GetServerSideProps = async (context) => {
// Use configuration in server-side code
const apiUrl = config.get<string>('api.baseUrl');
// Fetch data using configuration
const res = await fetch(`${apiUrl}/posts`);
const posts = await res.json();
return {
props: { posts }
};
};
In a monorepo, you might want to share configuration across multiple packages while allowing for package-specific overrides:
// packages/config/src/index.ts
import { createConfig, ConfigObject } from '@aipto/config';
// Base configuration for all packages
const baseConfig: ConfigObject = {
company: 'AIpto Inc.',
version: require('../../package.json').version,
logging: {
level: 'info',
format: 'json'
}
};
// Create a function to merge base configuration with package-specific config
export function createPackageConfig(packageConfig: ConfigObject): Config {
const config = createConfig(baseConfig);
config.update(packageConfig);
return config;
}
// packages/api/src/config.ts
import { createPackageConfig } from '@internal/config';
export default createPackageConfig({
name: 'api',
port: 3000,
routes: {
prefix: '/api/v1'
}
});
// packages/web/src/config.ts
import { createPackageConfig } from '@internal/config';
export default createPackageConfig({
name: 'web',
port: 8080,
publicPath: '/assets'
});
Here's a pattern for handling multiple deployment environments:
// config/environments/base.ts
export default {
app: {
name: 'AIpto Demo'
},
logging: {
level: 'info'
}
};
// config/environments/development.ts
import baseConfig from './base';
import { deepMerge } from '@aipto/config';
export default deepMerge(baseConfig, {
logging: {
level: 'debug'
},
database: {
host: 'localhost',
port: 5432
}
});
// config/environments/production.ts
import baseConfig from './base';
import { deepMerge } from '@aipto/config';
export default deepMerge(baseConfig, {
logging: {
level: 'warn'
},
database: {
host: 'db.production.example.com',
port: 5432
}
});
// config/index.ts
import { createConfig } from '@aipto/config';
import development from './environments/development';
import production from './environments/production';
import staging from './environments/staging';
const environments = {
development,
production,
staging
};
const env = process.env.NODE_ENV || 'development';
const config = createConfig(environments[env] || environments.development);
export default config;
If configuration updates don't seem to be taking effect:
// Check if you're modifying a copy instead of updating through the API
const config = createConfig({ value: 1 });
// Wrong - this modifies a copy, not the internal state
const values = config.getAll();
values.value = 2; // This won't affect the configuration
// Correct - use the API to update values
config.set('value', 2);
// Or
config.update({ value: 2 });
If you're getting type errors when accessing nested configuration values:
const config = createConfig({
database: {
port: 5432,
host: 'localhost'
}
});
// Type error: config.get('database') might be any type
const dbConfig = config.get('database');
console.log(dbConfig.port); // Error: Object is of type 'unknown'
// Solution 1: Type assertion
const dbConfig = config.get<{port: number, host: string}>('database');
console.log(dbConfig.port); // OK
// Solution 2: Define types
interface DbConfig {
port: number;
host: string;
}
const dbConfig = config.get<DbConfig>('database');
console.log(dbConfig.port); // OK
Feature | AIpto | node-config |
---|---|---|
TypeScript Support | Native | Via additional packages |
Schema Validation | Built-in | No built-in validation |
Browser Compatibility | Yes | No |
Configuration Sources | Extensible | File-based hierarchy |
Immutability | Optional | No |
Bundle Size | Small | Large |
Feature | AIpto | Convict |
---|---|---|
TypeScript Support | Native | Limited |
Schema Definition | JSON Schema | Custom format |
Nested Configuration | Dot notation | Dot notation |
Browser Compatibility | Yes | Limited |
External Dependencies | Minimal | Several |
Many projects use custom configuration solutions, which may lack:
- Type safety
- Validation
- Consistent API
- Documentation
- Testing
AIpto provides all these features out of the box, saving development time and reducing bugs.
Future features and improvements planned for AIpto:
- Configuration History: Track changes to configuration over time
- Remote Configuration: Load configuration from remote sources
- Configuration UI: Web interface for managing configurations
- Encrypted Values: Support for encrypted sensitive values
- Observability: Metrics and insights into configuration usage
- Migration Tooling: Tools for migrating between configuration versions
- Schema Generation: Automatic schema generation from TypeScript types
For detailed API documentation, please see the API Documentation file.
For information on contributing to AIpto, see the Development Guide.
We welcome contributions to AIpto! Please see our Contributing Guide for more information.
We are committed to providing a welcoming and inclusive environment. Please read our Code of Conduct before participating.
This project is licensed under the MIT License - see the LICENSE file for details.