Help us shape the future of Go! Take the 2026 Go Developer Survey

confx

package module
v0.5.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 10, 2026 License: MIT Imports: 7 Imported by: 0

README

confx

Go Reference CI Release

Explicit confmaker registration for Uber Fx. confx provides typed configs to Fx consumers. confmaker owns defaults, configuration engines, validation and diagnostics; the adapter owns wiring them into Fx.

Install · Quick start · Modules · Engines · Diagnostics · Manifest · Testing

Installation

Requires Go 1.27.0 or later. The command below installs the latest stable release. This README describes the current branch; for a pinned release, use its tagged documentation.

go get github.com/uchaloop/confx@latest
import "github.com/uchaloop/confx"

Import github.com/uchaloop/confmaker/v2 when using core options such as WithEnv, WithPrefix or WithDiagnostics.

Quick start

A domain declares its config and exposes one module. The config itself needs no Fx-specific methods:

package main

import (
	"fmt"

	"github.com/uchaloop/confx"
	"go.uber.org/fx"
)

type Config struct {
	Port int `env:"PORT"`
}

func (c *Config) SetDefaults() {
	c.Port = 8080
}

type service struct {
	port int
}

func makeService(cfg Config) *service {
	return &service{port: cfg.Port}
}

func main() {
	server := confx.MakeModule("server",
		confx.Provide[Config]("server"),
		confx.Runtime(
			fx.Provide(makeService),
			fx.Invoke(func(s *service) {
				fmt.Println("server port:", s.port)
			}),
		),
	)

	modules := confx.MakeModules(server)
	fx.New(modules.FX()).Run()
}

The built-in engine uses 8080 by default or reads SERVER_PORT. In a real application, each domain exports its own MakeModule result; main lists those modules once. FX() installs the adapter, so no additional confx.Module() is needed. Service constructors consume config values normally.

How it works

Provide and ProvideNamed preserve config declarations beside their Fx providers. MakeModule preserves nesting and scopes; MakeModules assembles the application. During fx.New, confmaker registers and loads all configurations, including unused ones, before any config value is provided to a consumer.

Defaults, decoding, validation and reports belong to confmaker. confx owns Fx registration and typed injection. Loading happens before lifecycle OnStart; this adapter does not start engine resources or add cancellation to Fx construction.

Use Runtime for ordinary providers, invocations and decorators. It marks the parts of the module tree that offline description must skip. It does not delay fx.Invoke until Start: normal Fx construction semantics still apply.

Modules and instances

Nest MakeModule values to preserve Fx private-provider visibility and decorator scopes. A plain fx.Module hides declarations from offline inspection; use MakeModule on configuration-bearing branches and Runtime on service-only branches.

An instance name, an ENV prefix and an Fx tag serve different purposes:

Concept Source Purpose
Instance name First argument to Provide / ProvideNamed Core registration identity and diagnostics
ENV prefix (built-in engine) Derived from the name, or WithPrefix Variable lookup
Fx name tag ProvideNamed only Select a named dependency

Using Config from the quick start, replace its registration with:

confx.Provide[Config]("server"),
confx.ProvideNamed[Config]("replica", confmaker.WithPrefix("READ_SERVER_")),

The first reads SERVER_PORT, the second READ_SERVER_PORT. Consume them with:

fx.Invoke(func(params struct {
	fx.In

	Primary Config
	Replica Config `name:"replica"`
}) {
	fmt.Println(params.Primary.Port, params.Replica.Port)
})

The same parameter shape can be used by a constructor. Two untagged providers of the same type conflict in Fx even if their instance names differ. WithPrefix never changes the Fx tag.

Configuration engines

The default engine reads ENV with confmaker tags and rules. Configure all domains through the composition, for example modules.WithOptions(confmaker.WithEnv(env)). WithOptions returns a copy and appends options; it does not mutate the original.

A custom engine is selected once for the same composition:

modules := confx.MakeModules(
	orders.Module,
	billing.Module,
).WithOptions(
	confmaker.WithEngine(engine),
	confmaker.WithDiagnosticHandler(handleReport),
)

The custom engine owns its sources, tags and decoding rules. confmaker still applies root defaults before decoding and validates afterward. Built-in options such as WithEnv and WithPrefix cannot be combined with a custom engine.

Runnable example What it demonstrates
env/v11 Native ENV tags and envDefault, isolated input maps, two named configs
koanf + TOML Embedded TOML files, overlay settings, native koanf tags, two named configs

Both examples build a domain module and inject configurations into a service. Their dependencies live in a separate Go module, not in the adapter's main module. The package GoDoc also includes a runnable standard-library JSON engine example. For Viper and direct YAML/TOML decoders, see confmaker's engine examples.

Each Fx application receives its own Loader, but reused Engine objects and callback closures are shared. Engines must support concurrent calls if reused concurrently. The application owns their resources. Backend errors may contain input values; the diagnostic report contains no configuration values. Custom-engine reports have config-level detail and their Manifest returns ErrManifestUnsupported.

Errors and diagnostics

Use modules.FX(), or exactly one legacy confx.Module() / confx.FromLoader(loader). All provided configs load during fx.New, including those with no consumers. A core loading error is available through app.Err() and prevents startup.

Core structured errors remain reachable through Fx's wrappers:

if err := app.Err(); err != nil {
	for _, problem := range confmaker.ConfigErrors(err) {
		fmt.Println(problem.Kind, problem.InstanceName, problem.VariableName)
	}
	log.Fatal(err)
}

Always handle the original error: missing Module and Fx dependency conflicts are not core ConfigError diagnostics. Config values should be treated as read-only. With Module, Provide and ProvideNamed options may be reused across applications; each app receives its own loader and values. External loaders retain shared values and load state, as described below.

Use confmaker.WithDiagnosticHandler(func(report confmaker.LoadReport) { ... }) through modules.WithOptions (or the standalone Module) for a value-free report during app construction, including load errors. With an external loader, WithDiagnostics() enables loader.Report() snapshots. The loader owns the report; there is no separate receiver. Callbacks run after publication and do not run for loading panics.

If Fx rejects the graph before loading, the diagnostic callback does not run. An enabled external loader remains not_started. Inspect app.Err() for graph errors; reports describe configuration loading, not the entire Fx application.

Manifest and describe

Declare configuration once alongside the domain services:

var Module = confx.MakeModule("orders",
	confx.Provide[Config]("orders"),
	confx.Runtime(
		fx.Provide(makeService),
		fx.Invoke(registerHandlers),
	),
)

Compose the modules once in your application. describe below is a parsed confcli.MakeDescribeFlag; flag parsing stays with the application.

modules := confx.MakeModules(
	orders.Module,
	billing.Module,
).WithOptions(
	confmaker.WithDiagnosticHandler(handleReport),
)

if describe.Requested() {
	manifest, err := modules.Manifest(confmaker.IncludeDefaults())
	if err != nil {
		log.Fatal(err)
	}

	if err := describe.Write(os.Stdout, manifest); err != nil {
		log.Fatal(err)
	}

	return
}

fx.New(modules.FX()).Run()

FX() installs the core adapter itself: do not add confx.Module() or FromLoader() alongside it. Each application receives a fresh loader using the same saved options. WithOptions returns a new composition; supplied engines and callback closures are shared. Nested MakeModule preserves Fx private visibility and decorator scopes.

Manifest does not construct an Fx application, load ENV, invoke services or call load diagnostics. IncludeDefaults explicitly executes configuration defaults and renderers. It does not validate the service graph or all load-only options. Custom engines still return confmaker.ErrManifestUnsupported.

To avoid silently incomplete output, Manifest rejects every opaque option outside Runtime, including ordinary fx.Options, fx.Module and fx.Provide. The error wraps confx.ErrOpaqueOption and identifies the module path and option index. The entire tree is checked before defaults run; errors return no partial manifest. Such ordinary Fx options remain usable on the FX() startup path.

Runtime asserts that its options contain no configuration declarations. Direct confx registrations, module specifications and legacy loader/handle options are rejected inside it. A registration hidden deeper inside an opaque Fx wrapper cannot be detected: do not put configuration-bearing wrappers inside Runtime. Argument evaluation, such as fx.Supply(makeService()), and Go package init execute normally and are not suppressed by description mode.

The original Module/Provide and external-loader APIs remain available when application-level composition or offline description is unnecessary.

External loader

Use an external Loader when your application already owns configuration registration, loading or inspection. FromLoader and the handle adapters bridge those registrations into Fx without registering them again.

Adapter Purpose
FromLoader(loader) Use an existing loader instead of Module
FromHandle(handle) Provide an existing registration as an untagged value
FromHandleNamed(handle) Provide it with its registration name as the Fx tag

All registrations load, even when no handle has a consumer. Handles must belong to the supplied loader; nil, zero and foreign handles fail construction. An already-loaded loader reuses its result. Reusing an external loader across apps also shares its values and load state; create separate loaders for isolation.

The two modes cannot be mixed: Module accepts only Provide/ProvideNamed; FromLoader accepts only FromHandle/FromHandleNamed. Mode and handle ownership checks finish before any registration or loading. Invalid connections leave the external loader unchanged, so a corrected connection can reuse it. Once Load has run, its result remains final as usual.

Register every config outside Fx in the extended mode. The manifest generated before fx.New then describes the complete registration set.

For export formats and guarantees, see the core manifest guide. The adapters do not parse flags or terminate the process.

Testing

Pass an isolated ENV map to Module. This setup uses Config above and imports testing, confmaker, confx and Fx:

func TestConfig(t *testing.T) {
	t.Parallel()

	var got Config
	app := fx.New(
		fx.NopLogger,
		confx.Module(confmaker.WithEnv(map[string]string{"SERVER_PORT": "9090"})),
		confx.Provide[Config]("server"),
		fx.Populate(&got),
	)
	if err := app.Err(); err != nil {
		t.Fatal(err)
	}
	if got.Port != 9090 {
		t.Fatalf("unexpected port: %d", got.Port)
	}
}

No app.Start is needed to inspect configuration values: loading already happened during construction. In a real app, unrelated constructors can run during fx.New; this is not a general-purpose dry-run mode.

For repository checks, run each module explicitly with GOWORK=off: the root, integration/secret and examples/engines. In each directory run:

GOWORK=off go mod tidy -diff
GOWORK=off go build ./...
GOWORK=off go vet ./...
GOWORK=off go test -race ./...

Real secret compatibility lives in integration/secret. The root module does not depend on secret or the example engines. See CONTRIBUTING.md for code style and contribution requirements.

Reference

Acknowledgements

Thanks to the authors and maintainers of Uber Fx for its dependency injection and lifecycle primitives.

License

MIT

Documentation

Overview

Package confx adapts github.com/uchaloop/confmaker/v2 to Uber Fx. The application explicitly registers its package configs; confmaker owns configuration engines, defaults, validation and diagnostics, while confx provides the resulting typed values. The adapter supports both the built-in ENV engine and application-supplied engines.

Import github.com/uchaloop/confx for registration and github.com/uchaloop/confmaker/v2 when using core options such as WithEnv or WithPrefix.

Quick start

Declare configuration in the domain and place ordinary Fx options in Runtime:

var Module = confx.MakeModule("store",
	confx.Provide[Config]("store"),
	confx.Runtime(fx.Provide(makeService)),
)

Compose domain modules once:

modules := confx.MakeModules(store.Module, orders.Module)
fx.New(modules.FX()).Run()

FX installs one core adapter per application. Do not add Module or FromLoader alongside it. WithOptions returns a new composition with shared core loader options; each Fx application gets a fresh loader.

Registration and instances

Provide supplies an untagged value; ProvideNamed uses the registration name as its Fx name tag. With the built-in engine, the name also determines the ENV prefix; WithPrefix overrides only that prefix. A custom engine owns source lookup and tags. The original Module plus Provide API remains available for ordinary Fx composition when offline manifest collection is not needed.

Lifecycle

Every provided config is loaded during fx.New, including configs with no consumer. Loading errors are available from fx.App.Err and prevent startup. Options returned by Provide and ProvideNamed may be reused across applications; each application has its own loader and config values.

Multiple instances and errors

Use Provide for an untagged value. Two values of the same Go type need distinct Fx identities, even when their ENV prefixes differ. ProvideNamed uses the instance name as that identity:

fx.Invoke(func(params struct {
	fx.In

	Primary postgres.Config
	Replica postgres.Config `name:"replica"`
}) {
	fmt.Println(params.Primary.Host, params.Replica.Host)
})

Inspect construction errors with app.Err(). Core ConfigError diagnostics remain reachable through confmaker.ConfigErrors and errors.As. Missing Module or conflicting Fx providers are adapter or Fx errors, not core config diagnostics. Loaded configs should be treated as read-only, including their maps and slices.

External registrations

FromLoader uses a caller-owned confmaker.Loader. FromHandle and FromHandleNamed provide existing handles without registering them again. A named handle uses Handle.Name as its Fx tag. All handles must belong to the same loader. Exactly one Module or FromLoader may be installed.

For offline documentation, register configs externally and generate the manifest before fx.New. Normal startup can then use Run or explicit Start/Stop. Module accepts only Provide/ProvideNamed, while FromLoader accepts only FromHandle/FromHandleNamed. Mixing modes or supplying a foreign handle fails before registrations or loading and leaves the external loader unchanged. Previously loaded loaders retain their results. Reusing an external loader across apps shares its load state and values. The adapter does not interpret command-line flags or exit the process.

Custom engines

Module accepts confmaker.LoaderOption, including confmaker.WithEngine. An external loader can select the same backend. Every app has its own loader, but reusing a Module option shares the supplied Engine object, which must support concurrent calls across loaders. Its resources remain caller-owned. Loading still occurs at fx.New; this adapter adds no context or backend resource lifecycle. Custom engines provide config-level reports and do not support Loader.Manifest. Names accepted by the core remain subject to Fx named-tag restrictions. Runnable env/v11 and koanf + TOML applications live in the separate examples/engines module. Their dependencies are not part of this package. ExampleModule_customEngine demonstrates a standard-library JSON decoder.

Load diagnostics

Pass confmaker.WithDiagnosticHandler through WithOptions or Module for a value-free report before a loading error reaches Fx, preserving fx.New(...).Run(). For explicit inspection, enable confmaker.WithDiagnostics() on an external loader and read loader.Report after fx.New, before Run. External loaders accept the same core options. If Fx rejects the graph before loading, the handler is not called and an enabled loader report stays in the not_started state.

Module composition and offline description

MakeModule preserves configuration declarations beside a real Fx module. MakeModules composes one tree for Manifest and FX; WithOptions returns a copy with shared core loader options. FX installs Module itself and creates a fresh loader per application. Do not combine it with Module or FromLoader.

Wrap service providers, invocations and decorators in Runtime. Manifest rejects opaque options outside Runtime with ErrOpaqueOption, before executing defaults, and returns no partial result. Runtime asserts that its contents have no config registrations; configurations hidden in opaque Fx wrappers cannot be discovered. Nested MakeModule preserves module scopes. Manifest runs no Fx constructors or invocations; IncludeDefaults still runs user defaults and renderers. It neither validates the Fx graph nor all load-only options. Go argument evaluation and init are outside this guarantee. Custom engines do not support Manifest.

Core library

The core also works without Fx:

cfg, err := confmaker.Load[postgres.Config]("postgres")

Declaring configs, options, the manifest and loading rules are documented in package github.com/uchaloop/confmaker/v2.

Example

Provide makes the config available to constructors. WithEnv stands in for the process environment here; an application calls confx.Module() without it.

package main

import (
	"fmt"
	"time"

	"github.com/uchaloop/confmaker/v2"
	"github.com/uchaloop/confx"
	"go.uber.org/fx"
)

// StoreConfig is the kind of config a library declares: plain fields, env tags,
// and no knowledge of where the values come from.
type StoreConfig struct {
	Host    string        `env:"HOST,notEmpty"`
	Timeout time.Duration `env:"TIMEOUT"`
}

func (c *StoreConfig) SetDefaults() { c.Timeout = 30 * time.Second }

func main() {
	env := confmaker.WithEnv(map[string]string{"STORE_HOST": "db:5432"})

	app := fx.New(
		fx.NopLogger,
		confx.Module(env),
		confx.Provide[StoreConfig]("store"),
		fx.Invoke(func(cfg StoreConfig) {
			fmt.Println(cfg.Host, cfg.Timeout)
		}),
	)
	if err := app.Err(); err != nil {
		fmt.Println(err)
	}

}
Output:
db:5432 30s

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrOpaqueOption = errors.New("confx: opaque Fx option")

ErrOpaqueOption means Manifest cannot inspect an option. Use MakeModule for configuration modules and Runtime for options containing no registrations.

Functions

func FromHandle added in v0.3.0

func FromHandle[T any](handle *confmaker.Handle[T]) fx.Option

FromHandle provides an existing registration as an untagged T. FromLoader must reference its owner. No second registration is made. Nil, zero and foreign handles fail app construction before loading the application's configs.

func FromHandleNamed added in v0.3.0

func FromHandleNamed[T any](handle *confmaker.Handle[T]) fx.Option

FromHandleNamed provides an existing registration with its registration name as the Fx name tag. WithPrefix does not affect this name. FromLoader is required.

func FromLoader added in v0.3.0

func FromLoader(loader *confmaker.Loader) fx.Option

FromLoader uses an existing loader instead of Module. Use only FromHandle or FromHandleNamed with it; Provide registrations are rejected before loading. Load runs during fx.New, even without config consumers. A previously loaded loader retains its result and refuses new registrations. Include exactly one Module or FromLoader per app. Sharing an external loader across apps shares its values and one-shot load state.

func Module

func Module(opts ...confmaker.LoaderOption) fx.Option

Module loads the configs registered by Provide and ProvideNamed through one confmaker.Loader per application, configured by opts. Loading happens once during fx.New, when a constructor or invocation requests a config. Every registration is checked, including unused configs. Loading errors reach fx.App.Err and prevent startup, regardless of registration order.

Include exactly one Module or FromLoader. Module also checks its options when no configs are registered.

Example (CustomEngine)
package main

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/uchaloop/confmaker/v2"
	"github.com/uchaloop/confx"
	"go.uber.org/fx"
)

func main() {
	type config struct {
		Port int `json:"port"`
	}

	engine := confmaker.EngineFunc(func(ctx context.Context, req confmaker.LoadRequest) error {
		if err := ctx.Err(); err != nil {
			return err
		}

		if req.Name != "server" {
			return fmt.Errorf("unknown configuration %q", req.Name)
		}

		return json.Unmarshal([]byte(`{"port":9090}`), req.Target)
	})

	app := fx.New(
		fx.NopLogger,
		confx.Module(confmaker.WithEngine(engine)),
		confx.Provide[config]("server"),
		fx.Invoke(func(cfg config) {
			fmt.Println(cfg.Port)
		}),
	)
	if err := app.Err(); err != nil {
		fmt.Println(err)
	}

}
Output:
9090

func Provide

func Provide[T any](name string, opts ...confmaker.ConfigOption) fx.Option

Provide registers a config with the required instance name and provides it without an Fx tag. With the built-in engine, confmaker validates the name and derives its ENV prefix; confmaker.WithPrefix overrides only the prefix. Module is required; FromLoader cannot be mixed with Provide. The config is loaded during fx.New even when no constructor consumes it.

func ProvideNamed

func ProvideNamed[T any](name string, opts ...confmaker.ConfigOption) fx.Option

ProvideNamed registers a config with the required instance name and provides it with the Fx tag name:"<name>". With the built-in engine, the name determines the default prefix; confmaker.WithPrefix can override the prefix. Module is required, and loading happens during fx.New as for Provide.

Example

ProvideNamed provides a second instance of the same type under an Fx tag, with its own prefix.

package main

import (
	"fmt"
	"time"

	"github.com/uchaloop/confmaker/v2"
	"github.com/uchaloop/confx"
	"go.uber.org/fx"
)

// StoreConfig is the kind of config a library declares: plain fields, env tags,
// and no knowledge of where the values come from.
type StoreConfig struct {
	Host    string        `env:"HOST,notEmpty"`
	Timeout time.Duration `env:"TIMEOUT"`
}

func (c *StoreConfig) SetDefaults() { c.Timeout = 30 * time.Second }

func main() {
	env := confmaker.WithEnv(map[string]string{
		"STORE_HOST":         "db:5432",
		"REPLICA_STORE_HOST": "replica:5432",
	})

	app := fx.New(
		fx.NopLogger,
		confx.Module(env),
		confx.Provide[StoreConfig]("store"),
		confx.ProvideNamed[StoreConfig]("replica", confmaker.WithPrefix("REPLICA_STORE_")),
		fx.Invoke(func(p struct {
			fx.In

			Primary StoreConfig
			Replica StoreConfig `name:"replica"`
		}) {
			fmt.Println(p.Primary.Host, p.Replica.Host)
		}),
	)
	if err := app.Err(); err != nil {
		fmt.Println(err)
	}

}
Output:
db:5432 replica:5432

func Runtime added in v0.5.1

func Runtime(opts ...fx.Option) fx.Option

Runtime marks options as containing no configuration declarations. Manifest skips them; Fx applies them normally in their original module scope. Known direct configuration options and nil options are rejected. Registrations hidden inside opaque fx.Options or fx.Module cannot be detected: callers must not hide them here. Argument evaluation and package init run normally in Go.

Types

type ModuleSpec added in v0.5.1

type ModuleSpec struct {
	// contains filtered or unexported fields
}

ModuleSpec preserves configuration declarations alongside a real Fx module. Create it with MakeModule. Its zero value is invalid.

func MakeModule added in v0.5.1

func MakeModule(name string, opts ...fx.Option) *ModuleSpec

MakeModule groups declarations and runtime options, preserving Fx scopes. Manifest rejects opaque options unless explicitly enclosed in Runtime.

type Modules added in v0.5.1

type Modules struct {
	// contains filtered or unexported fields
}

Modules is an immutable application composition. Use MakeModules to create it. A zero Modules describes an empty application.

func MakeModules added in v0.5.1

func MakeModules(modules ...fx.Option) *Modules

MakeModules captures one module list for Manifest and FX.

Example

One module tree is used for offline description and normal Fx construction.

package main

import (
	"fmt"
	"time"

	"github.com/uchaloop/confmaker/v2"
	"github.com/uchaloop/confx"
	"go.uber.org/fx"
)

// StoreConfig is the kind of config a library declares: plain fields, env tags,
// and no knowledge of where the values come from.
type StoreConfig struct {
	Host    string        `env:"HOST,notEmpty"`
	Timeout time.Duration `env:"TIMEOUT"`
}

func (c *StoreConfig) SetDefaults() { c.Timeout = 30 * time.Second }

func main() {
	orders := confx.MakeModule("orders",
		confx.Provide[StoreConfig]("store"),
		confx.Runtime(
			fx.Invoke(func(cfg StoreConfig) {
				fmt.Println("loaded:", cfg.Host)
			}),
		),
	)

	modules := confx.MakeModules(orders).WithOptions(
		confmaker.WithEnv(map[string]string{"STORE_HOST": "db:5432"}),
	)

	manifest, err := modules.Manifest()
	if err != nil {
		fmt.Println(err)

		return
	}

	fmt.Println("described:", manifest.Configs[0].InstanceName)

	app := fx.New(fx.NopLogger, modules.FX())
	if err := app.Err(); err != nil {
		fmt.Println(err)
	}

}
Output:
described: store
loaded: db:5432

func (*Modules) FX added in v0.5.1

func (m *Modules) FX() fx.Option

FX builds an Fx option with one fresh loader per application. Do not combine it with Module or FromLoader. Ordinary opaque Fx options work in this path.

func (*Modules) Manifest added in v0.5.1

Manifest describes all visible declarations without constructing an Fx app. It first rejects opaque branches, then delegates to the core. IncludeDefaults executes user defaults and renderers. This does not validate the Fx graph or all load-only options. A custom engine returns ErrManifestUnsupported.

func (*Modules) WithOptions added in v0.5.1

func (m *Modules) WithOptions(opts ...confmaker.LoaderOption) *Modules

WithOptions returns a copy with additional shared core options. Engine objects and callback closures are shared; the loader itself is never shared.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL