A modern config for Dart
Let’s say you have an app, a CLI command, or a server. You’re handling a couple of command-line arguments to specify verbose mode, the hostname of a server API you need to connect with, and a port number. You might be getting them directly from the argument array, or maybe you’re using the basic args package.
That URL gets cumbersome for your users to specify every time the program is run. You want to be able to fetch it from environment variables. So you add code for that. And you also have to add code to handle when users specify both args and environment variables, and parse the formatting properly. You add good error messages and a bunch of tests that verify all the combinations that can occur. (Or you don’t add tests and your users get …surprised.)
Then, the host and port aren’t enough; you need to add some more settings. Actually, supporting a proper configuration file starts looking like the best experience for your users. How hard can it be, right? 100 lines of code and 200 lines of tests later, you handle most YAML cases. Surely, the users won’t get the idea to specify the port number as a string, right? Or you’ve done configuration file parsing before, don’t feel like doing it right now, and that ticket is postponed yet again.
About us
Serverpod has thousands of users worldwide, and our CLI tools are an essential part of our user experience. That sets a high bar for capabilities, robustness, and ease of use for a command that starts with argument and configuration handling.
In the course of developing these tools, we’ve well outgrown the standard Dart args package. In fact, we’ve created a quite comprehensive library, which we call Config, that wraps args and adds a ton of nice features. With all the capabilities we needed, it wasn’t possible to extend or retrofit the args package; we needed a new API.
Here, we present the features and API design of our new tooling. We’re an open-source company, and the config library is one of our contributions to the Dart ecosystem.
Key features
The key features are:
- Typed options:
int,DateTime,Duration, user-defined enums, etc.
– Automatic parsing and user-friendly error messages.
– Type-specific constraints, such as min/max for allComparableoption types.
– Multivalued options are typed, e.g.,List<MyEnum>.
– Custom types can easily be added and combined with the existing ones. - Equal support for positional arguments, with proper validation.
– Arguments can be both positional and named, making the — name optional. - Equal support for environment variables.
– Options can be specified both via arguments and environment variables.
– Environment variables have the same support for typed values as args. - Options can be fetched from configuration files as well.
– YAML/JSON configuration file support. - Options can have custom value-providing callbacks.
- Named option groups are supported.
– A group can specify mutually exclusive options.
– A group can be mandatory in that at least one of its options is set. - Tracability — the information on an option’s value source is retained.
- The error handling is consistent, in contrast to the args package.
– Fail-fast, all validation is performed up-front.
– All errors are collected, avoiding the poor UX of fix-one-and-then-get-the-next-error.
– Well-defined exception behavior.
This library is developed for the Serverpod CLI but can be used in any Dart project. It can also be used outside command-line argument handling, for example, in projects that just need a structured way of reading environment variables or parsing configuration files.
Installation
The Config library is distributed in Serverpod’s cli_tools package on pub.dev.
Configuration as an envelope
Consider an application that runs with various settings and modes.
- Some are mandatory, others are optional or have defaults.
- Some require special parsing, others have constraints on what values are valid.
- Some are usually specified in configuration files, but need to be overridable by the user.
- Some configuration files are typically stored in the user’s home directory but can be overridden by a file in the project directory.
- Some values need to be fetched from an API, like credentials from a secrets store.
- Another setting may provide the secret store address, which means that settings can be dependent on other settings.
Regardless of the source, the settings together constitute a configuration envelope for the application. For example, there is a development envelope and a production envelope, with different resulting behaviors.
The point is that the application shouldn’t care where a setting comes from; it should just care that its value is available and valid from the envelope.

API goals
Fail-fast
The configuration resolution should fail right away on input errors, with good error messaging to the user. (The args package does not do this; many errors are only signalled upon actual option access.)
Typed values
The values returned should be typed as the common Dart types — int, DateTime, etc. Including standard types parsing in a configuration library is an obvious case of implement-once-use-everywhere.
Typed configuration
The resulting configuration envelope should be a typed Configuration object, which provides flexible ways to access and search the options and is also extendable.
Declarative over procedural
The configuration option definition is like a data schema definition. It is ideally specified declaratively, which is more concise and less error-prone.
Don’t mix configuration parsing with business logic
An application that has grown over time often has argument and environment variable parsing code sprinkled around the code base. The configuration should rather be parsed and validated in a one-off operation, then passed as typed Dart values to the business logic.
Minimal boilerplate
Parsing arguments and environment variables is not rocket science. Any configuration library should feel lightweight.
Avoid API cliffs and enable advanced customization
Another drawback with the args package is that several behaviors are hardcoded with no way of changing them without extending and reimplementing lots of its functionality. This is referred to as an API cliff, where it’s easy to get going, but then you reach a cliff that you can’t overcome with reasonable effort.
Predictable behavior, including exceptions and error messages
Well-defined behavior with designated exceptions and return values makes it easier to use a library to make the application robust and produce good feedback to the developer and end-user.
Easy transition from legacy package
While not directly extending the args package, the new config library should be easy to transition to for users with existing code using args — ideally with the possibility of a drop-in replacement.
Example usage
Here are real-life code snippets, from a show logs command, on how to create a set of options for a particular command in the form of an enum.
import 'package:cli_tools/config.dart';
enum LogOption<V> implements OptionDefinition<V> {
limit(IntOption(
argName: 'limit',
helpText: 'The maximum number of log records to fetch.',
defaultsTo: 50,
min: 0,
)),
utc(FlagOption(
argName: 'utc',
argAbbrev: 'u',
helpText: 'Display timestamps in UTC timezone instead of local.',
defaultsTo: false,
envName: 'DISPLAY_UTC',
)),
recent(DurationOption(
argName: 'recent',
argAbbrev: 'r',
argPos: 0,
helpText:
'Fetch records from the recent period. '
'Can also be specified as the first argument.',
min: Duration.zero,
)),
before(DateTimeOption(
argName: 'before',
helpText: 'Fetch records from before this timestamp.',
));
const LogOption(this.option);
@override
final ConfigOptionBase<V> option;
}
The enum form enables constant initialization, typed Configuration<LogType>, and easy reference.
Resolution
Each option value is resolved in a specific order, with earlier source types taking precedence over later ones.
Command-line arguments
- Named arguments (e.g., — verbose or -v) have top precedence
- Positional arguments are resolved after named
- Specified using
argName,argAbbrev, andargPos
Environment variables
- Environment variables have second precedence after CLI arguments
- The variable name is specified using
envName
Configuration files
- Values from configuration files (e.g., YAML/JSON)
- Lookup key is specified using
configKey
Custom value providers
- Values from custom callbacks
- Callbacks are allowed to depend on other option values (option definition order is significant in this case)
- Callback is specified using
fromCustom
Default values
- A default value guarantees that an option has a value
- Constant values are specified using
defaultsTo - Non-constant values are specified with a callback using
fromDefault
This order ensures that:
- Command-line arguments always take precedence
- Environment variables can be used for values used across multiple command invocations, or to override other configuration sources
- Configuration files provide persistent settings storage
- Custom providers enable complex logic and integration with external systems
- Default values serve as a fallback when no other value is specified
This is an overview of how a Configuration is resolved:

Flexible sources
Only the value sources provided to the Configuration.resolve constructor are included. This means that only one or some source types need to be specified, for example, a configuration resolved purely from a map of environment variables. This enables flexible inclusion of sources depending on context, and helps construct specific test cases.
Supported option types
The library provides an extensive set of option value types out of the box.

It is easy to add custom option types and reuse the parsing code from existing option types. Just copy the code from existing options and modify it as needed.
More information
See the main README in GitHub, a complete configuration file example, and the API docs on pub.dev. And as with all open source projects, please contribute your feedback and improvements.