ConfigMate is a lightweight, type-safe, production-ready configuration loader for Go applications. Load configurations from .env, JSON, and YAML files with built-in defaults, validation, hot-reload, and layered overrides.
- π― Type-Safe - Struct-based configuration with compile-time type checking
- π Multi-Source - Load from
.env, JSON, and YAML files - π Hot Reload - Watch files for changes and reload automatically
- β Validation - Required field validation and default values via struct tags
- π Secure - Built-in security checks (path traversal, file size limits)
- π Production-Ready - Structured logging, metrics, and context support
- ποΈ Flexible - Environment variable overrides with custom prefixes
- πΎ Caching - Optional configuration caching with TTL
- π§ͺ Well-Tested - Comprehensive test suite with >85% coverage
- π¦ Minimal Dependencies - Only YAML parser and file watcher
go get github.com/psmohan/configmatepackage main
import (
"log"
"github.com/psmohan/configmate/pkg/configmate"
)
type Config struct {
AppName string `config:"app_name,default=MyApp"`
Port int `config:"port,default=8080"`
Debug bool `config:"debug,default=false"`
Database DatabaseConfig `config:"database"`
}
type DatabaseConfig struct {
Host string `config:"host,required"`
Port int `config:"port,default=5432"`
Username string `config:"username,required"`
Password string `config:"password,required"`
}config.yaml:
app_name: "MyApp"
port: 8080
debug: true
database:
host: "localhost"
port: 5432
username: "admin"
password: "secret".env:
APP_NAME=MyApp
PORT=8080
DEBUG=true
DATABASE_HOST=localhost
DATABASE_USERNAME=admin
DATABASE_PASSWORD=secretfunc main() {
var cfg Config
// Simple load
if err := configmate.Load(&cfg, ".env", "config.yaml"); err != nil {
log.Fatal(err)
}
// Use your config
log.Printf("Starting %s on port %d", cfg.AppName, cfg.Port)
}var cfg Config
// Load from single source
err := configmate.Load(&cfg, "config.yaml")
// Load from multiple sources (later sources override earlier ones)
err := configmate.Load(&cfg, ".env", "config.yaml")
// Panic on error (useful for startup)
configmate.MustLoad(&cfg, "config.yaml")loader := configmate.NewLoader(
configmate.WithPrefix("APP"), // ENV prefix: APP_PORT
configmate.WithWatch(true), // Enable hot reload
configmate.WithCache(true), // Enable caching
configmate.WithCacheTTL(5*time.Minute), // Cache for 5 minutes
configmate.WithSecureMode(true), // Enable security checks
configmate.WithOnReload(func(cfg interface{}) {
log.Println("Config reloaded!")
}),
)
err := loader.Load(&cfg, "config.yaml")ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := loader.LoadWithContext(ctx, &cfg, "config.yaml")loader := configmate.NewLoader(
configmate.WithWatch(true),
configmate.WithOnReload(func(newCfg interface{}) {
c := newCfg.(*Config)
log.Printf("Config reloaded! New port: %d", c.Port)
}),
)
err := loader.Load(&cfg, "config.yaml")
// Config auto-reloads when file changeslogger := configmate.NewDefaultLogger(configmate.LogLevelInfo)
metrics := configmate.NewDefaultMetrics()
loader := configmate.NewLoader(
configmate.WithLogger(logger),
configmate.WithMetrics(metrics),
)
err := loader.Load(&cfg, "config.yaml")
// Check metrics
stats := metrics.GetStats()
fmt.Printf("Loads: %d, Errors: %d\n", stats.LoadCount, stats.ErrorCount)`config:"key_name,option1,option2"`| Option | Description | Example |
|---|---|---|
config:"name" |
Map to config key | config:"app_name" |
,default=value |
Set default value | config:"port,default=8080" |
,required |
Mark as required | config:"api_key,required" |
config:"-" |
Ignore field | config:"-" |
type Config struct {
// Custom key name
AppName string `config:"app_name"`
// With default value
Port int `config:"port,default=8080"`
// Required field
APIKey string `config:"api_key,required"`
// Required with default
DBHost string `config:"db_host,required,default=localhost"`
// Nested struct
Server ServerConfig `config:"server"`
// Ignored field
Internal string `config:"-"`
// Private field (auto-ignored)
private string
}ConfigMate loads configurations in this order (later sources override earlier):
1. Struct Defaults (from tags)
β
2. Config Files (in order provided)
β
3. Environment Variables (if EnvOverride=true, default)
Example:
loader.Load(&cfg, "config.yaml", ".env.local")
// Precedence: defaults < config.yaml < .env.local < ENV varsEnvironment variables are automatically mapped:
type Config struct {
Port int `config:"port"` // Maps to PORT or APP_PORT (with prefix)
}loader := configmate.NewLoader(
configmate.WithPrefix("APP"),
)
// PORT becomes APP_PORT
// DATABASE_URL becomes APP_DATABASE_URLtype Config struct {
Database DatabaseConfig `config:"database"`
}
type DatabaseConfig struct {
Host string `config:"host"`
Port int `config:"port"`
}
// Environment variables:
// DATABASE_HOST=localhost
// DATABASE_PORT=5432app_name: "MyApp"
port: 8080
debug: true
database:
host: "localhost"
port: 5432
servers:
- name: "server1"
port: 8081
- name: "server2"
port: 8082{
"app_name": "MyApp",
"port": 8080,
"debug": true,
"database": {
"host": "localhost",
"port": 5432
}
}# Application settings
APP_NAME=MyApp
PORT=8080
DEBUG=true
# Database
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_URL=postgresql://localhost:5432/mydbWhen WithSecureMode(true) is enabled:
- β
Path Traversal Protection - Prevents
../attacks - β File Size Limits - Default 10MB maximum
- β
Extension Validation - Only
.env,.json,.yaml,.yml - β Path Depth Limits - Prevents excessive nesting
loader := configmate.NewLoader(
configmate.WithSecureMode(true), // Enabled by default
)loader := configmate.NewLoader(
configmate.WithValidator(func(cfg interface{}) error {
c := cfg.(*Config)
if c.Port < 1024 {
return errors.New("port must be >= 1024")
}
return nil
}),
)loader := configmate.NewLoader(
// Environment variable prefix
configmate.WithPrefix("APP"),
// Enable file watching for hot reload
configmate.WithWatch(true),
// Callback when config reloads
configmate.WithOnReload(func(cfg interface{}) {
// Handle reload
}),
// Fail if config file not found
configmate.WithFailOnMissingFile(true),
// Environment variables override file config
configmate.WithEnvOverride(true),
// Custom logger
configmate.WithLogger(logger),
// Custom metrics collector
configmate.WithMetrics(metrics),
// Enable configuration caching
configmate.WithCache(true),
// Cache time-to-live
configmate.WithCacheTTL(5 * time.Minute),
// Custom validation function
configmate.WithValidator(validatorFunc),
// Enable security checks
configmate.WithSecureMode(true),
)// Create logger with log level
logger := configmate.NewDefaultLogger(configmate.LogLevelInfo)
loader := configmate.NewLoader(
configmate.WithLogger(logger),
)
// Log Levels: LogLevelDebug, LogLevelInfo, LogLevelWarn, LogLevelErrortype MyLogger struct{}
func (l *MyLogger) Debug(msg string, fields map[string]interface{}) {
// Your implementation
}
func (l *MyLogger) Info(msg string, fields map[string]interface{}) {
// Your implementation
}
func (l *MyLogger) Warn(msg string, fields map[string]interface{}) {
// Your implementation
}
func (l *MyLogger) Error(msg string, err error, fields map[string]interface{}) {
// Your implementation
}
// Use custom logger
loader := configmate.NewLoader(
configmate.WithLogger(&MyLogger{}),
)metrics := configmate.NewDefaultMetrics()
loader := configmate.NewLoader(
configmate.WithMetrics(metrics),
)
// Get metrics snapshot
stats := metrics.GetStats()
fmt.Printf("Load Count: %d\n", stats.LoadCount)
fmt.Printf("Error Count: %d\n", stats.ErrorCount)
fmt.Printf("Reload Count: %d\n", stats.ReloadCount)
fmt.Printf("Cache Hits: %d\n", stats.CacheHits)
fmt.Printf("Cache Misses: %d\n", stats.CacheMisses)
fmt.Printf("Average Duration: %v\n", stats.AverageDuration)# Run all tests
go test ./...
# With coverage
go test -v -race -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# Run benchmarks
go test -bench=. -benchmem ./...func TestMyApp(t *testing.T) {
// Create temp config
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "config.yaml")
content := `
app_name: "TestApp"
port: 9000
`
os.WriteFile(configPath, []byte(content), 0644)
// Load config
var cfg Config
err := configmate.Load(&cfg, configPath)
// Assert
assert.NoError(t, err)
assert.Equal(t, "TestApp", cfg.AppName)
assert.Equal(t, 9000, cfg.Port)
}// β
Good: Clear tags with defaults
type Config struct {
Port int `config:"port,default=8080"`
API string `config:"api_key,required"`
}
// β Avoid: No tags, unclear mapping
type Config struct {
Port int
API string
}// β
Good: Separate configs
type Config struct {
Server ServerConfig `config:"server"`
Database DatabaseConfig `config:"database"`
Cache CacheConfig `config:"cache"`
}
// β Avoid: Flat structure
type Config struct {
ServerHost string
ServerPort int
DBHost string
DBPort int
CacheHost string
CachePort int
}// Development
loader.Load(&cfg, ".env.development", "config.yaml")
// Production
loader.Load(&cfg, ".env.production", "config.yaml")
// Use ENV var to select
env := os.Getenv("APP_ENV")
loader.Load(&cfg, fmt.Sprintf(".env.%s", env), "config.yaml")func main() {
var cfg Config
loader := configmate.NewLoader(
configmate.WithWatch(true),
configmate.WithOnReload(func(newCfg interface{}) {
// Gracefully update running services
updateServer(newCfg.(*Config))
}),
)
loader.Load(&cfg, "config.yaml")
defer loader.Close() // Clean up watcher
// Run app...
}loader := configmate.NewLoader(
configmate.WithValidator(func(cfg interface{}) error {
c := cfg.(*Config)
// Validate port range
if c.Port < 1024 || c.Port > 65535 {
return errors.New("invalid port range")
}
// Validate database connection
if !strings.HasPrefix(c.Database.URL, "postgresql://") {
return errors.New("invalid database URL")
}
return nil
}),
)// Check file exists
if _, err := os.Stat("config.yaml"); os.IsNotExist(err) {
log.Fatal("config.yaml not found")
}
// Or use FailOnMissingFile
loader := configmate.NewLoader(
configmate.WithFailOnMissingFile(true),
)// Error: field 'APIKey': required field is missing or empty
// Solution: Check struct tag
type Config struct {
APIKey string `config:"api_key,required"` // Correct
}
// Ensure value in config file
// config.yaml:
// api_key: "your-key-here"// 1. Check EnvOverride is enabled (default: true)
loader := configmate.NewLoader(
configmate.WithEnvOverride(true),
)
// 2. Check variable name matches
// Struct: Port int `config:"port"`
// ENV: PORT=8080 (or APP_PORT=8080 with prefix)
// 3. Set environment variable
os.Setenv("PORT", "8080")# β
Correct YAML indentation (2 spaces)
database:
host: "localhost"
port: 5432
# β Wrong (tabs or wrong indentation)
database:
host: "localhost" # Tab character
port: 5432 # 3 spacesSee cmd/example/ for a complete working example.
cd cmd/example
go run main.goContributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
For questions or support, please open an issue on GitHub.
Happy Configuring! π