Skip to content

Configure an app

gas/config is the one module that is not a service. It is built and loaded before the app, so a bad configuration fails before anything else starts, and every other module binds its own settings through the gas.ConfigProvider you register.

Terminal window
go get github.com/gasmod/gas/config
main.go
// Config is built and loaded before the app, so a bad configuration fails
// before anything else starts. Later providers override earlier ones.
func load() *config.Config {
cfg := config.New(
config.WithProvider(providers.NewEnvProvider()), // lowest priority
config.WithProvider(providers.NewJSONProvider( // overrides env
providers.WithJSONFilePath("config.json"),
)),
config.WithExtension(gasenv.NewExtension()),
)
if err := cfg.Load(); err != nil {
log.Fatal(err)
}
return cfg
}
main.go
// Registered as an instance, so every module can bind its own settings.
func register(cfg *config.Config) gas.Option {
return gas.WithServiceInstance[gas.ConfigProvider](cfg)
}

Later providers override earlier ones, so the order above means environment variables are the base and the JSON file wins. Pick the order that matches how you deploy; a common one is env last so a container can override anything.

Keys are lowercased. By default they stay flat, so DATABASE_HOST becomes database_host. Use a double underscore to nest, which is what every Gas module expects:

Terminal window
DATABASE__DSN=postgres://localhost/app # database.dsn
STORAGE__BUCKET=uploads # storage.bucket
JWT__SIGNING_KEY=... # jwt.signing_key

A prefix is stripped literally, so include the trailing separator: WithEnvPrefix("APP_") turns APP_HOST into host, while WithEnvPrefix("APP") leaves you with _host.

// Environment variables are lowercased and flat by default. Use a double
// underscore to nest: DATABASE__HOST becomes database.host.
func prefixed() *providers.EnvProvider {
return providers.NewEnvProvider(
// The prefix is stripped literally, so include the trailing separator.
providers.WithEnvPrefix("APP_"),
providers.WithEnvSeparator("__"),
)
}
Provider Source
providers.NewEnvProvider() Process environment
providers.NewJSONProvider() A JSON file, from disk or an fs.FS
providers.NewDotEnvProvider() A .env file
secretsmanager.NewProvider() AWS Secrets Manager, fetched eagerly at load

Write your own by implementing Name() string and Load() (map[string]any, error). Add LoadContext(ctx) and LoadWithContext will pass its context through, which matters for anything doing network calls.

// Bind maps configuration into a struct, matching on json tags first and then
// case-insensitive field names, and validates it on the way through.
type AppConfig struct {
gasenv.WithGasEnv
Database struct {
Host string `json:"host"`
Port int `json:"port" validate:"required"`
} `json:"database"`
RequestTimeout time.Duration `json:"request_timeout"`
}
func bind(cfg *config.Config) (*AppConfig, error) {
var out AppConfig
if err := cfg.Bind(&out); err != nil {
return nil, err
}
return &out, nil
}

Fields match on json tags first, then case-insensitively by name. Supported: every int, uint, and float width, bool, string, time.Duration, slices, fixed-size arrays, maps, nested structs, and embedded structs. Comma-separated strings bind to slices; a fixed-size array needs a real list, and a source with more elements than fit is an error rather than a silent truncation.

Validation runs through go-playground/validator on validate tags. Pass config.WithValidate(false) to skip it, or config.WithValidator(v) to supply an instance with your own custom rules registered.

The gasenv extension resolves the current environment from config providers, then GAS_ENV, then a default of Development. Embed gasenv.WithGasEnv in your config struct and Bind populates it, giving you IsProduction(), IsDevelopmentLike(), and friends. Gas uses this itself, which is how gas/ui knows to rebuild templates on every request in development.

Core reads its own gas.Config for the HTTP server.

// The server settings Gas itself reads. DefaultConfig fills these in; override
// only what you need.
func serverConfig() *gas.Config {
c := gas.DefaultConfig()
c.Server.Port = 9090
c.Server.ShutdownTimeout = 30 * time.Second
return c
}
Field Default
Server.Host 0.0.0.0
Server.Port 8080
Server.ReadTimeout 5s
Server.WriteTimeout 10s
Server.IdleTimeout 2m
Server.ShutdownTimeout 30s

configtest.MockConfig records calls and takes per-method Fn overrides. When a test needs real Get/Find/Bind behaviour over known values, use configtest.NewMockConfigWithValues, which delegates to a real config and avoids leaking the machine’s environment into the test. Seed it with nested maps, not dotted keys: values go through SetDefaults, which does not split on ., so "database.host" would become one flat key that Get("database.host") cannot reach.