DEV Community

Cover image for Simplify your environment variable management
Baptiste Parmantier
Baptiste Parmantier

Posted on

Simplify your environment variable management

Introduction

Environment management is an extremely sensitive but almost systematic issue.

Whether you are working on a web, mobile or backend application, environment variables allow you to configure your application according to the environment in which it is being run (development, production, etc...).

However, managing these variables can quickly become complex, especially when you need to validate their format, type or even presence.


Features

  1. Unified API
    A single method lets you access your environment variables, so you don't have to deal with multiple getters to retrieve your variables.

  2. Environment variable validation
    Define validation schemes for your environment variables. You can specify the expected data type (string, number, boolean, etc..), as well as additional rules (for example, checking whether a number is an integer or a double).

  3. Error management
    The package includes a robust error reporting system. If an environment variable does not comply with the defined rules, the package generates detailed errors, making debugging easier.

  4. Flexibility
    Designed to be extensible, you can add custom rules to meet specific needs, such as value transformation or enumeration validation.

  5. Support for .env files
    The package supports .env files, allowing you to load environment variables from local configuration files. It also manages the priority of environment-specific files (.env, .env.production...).


Environment management in Dart

Following the example of the Javascript environment, the package introduces an environment variable named DART_ENV which will have the same role as NODE_ENV present in Node applications.

This environment variable can be supplied using your application's startup command.

dart run --define=DART_ENV=development bin/entrypoint.dart
Enter fullscreen mode Exit fullscreen mode

Within your Dart application, you will need to write code similar to this.

void main() {
  final dartEnv = String.fromEnvironment('DART_ENV');
  print(dartEnv); // development;
}
Enter fullscreen mode Exit fullscreen mode

When you want to use a variable from the environment, you will have to pass them via command parameters.

It is your responsibility to type your variables correctly when retrieving them, by changing the primitive used to access your variable.

String.fromEnvironment('STRING_VARIABLE');
bool.fromEnvironment('BOOLEAN_VARIABLE');
int.fromEnvironment('INT_VARIABLE');
Enter fullscreen mode Exit fullscreen mode

An exception is thrown if the primitive type doesn't match the environment variable type prefix.

If an environment variable is not set, it defaults to the null value.

More details on official documentation.


User friendly

While the environment variable has not been retrieved yet by the primitive function call (fromEnvironment), there is no validation of the types, nor if the environment variable exists.

The env_guard package solves this problem by providing a builder that allows you to contractually declare your environment variables, ensuring that they are present (or not) but also ensuring their type.

Let's look at our environment variables in a .env file at the root of our project, or injected directly by Kubernetes or another technology.

HOST=127.0.0.1
PORT=3333
DEBUG=true
Enter fullscreen mode Exit fullscreen mode

To validate our environment, we'll use the define() method, which performs two actions :

  1. Load and parse the environment variables
  2. Validate it and persists the types
import 'package:env_guard/env_guard.dart';

void main() {
  env.define({
    'HOST': env.string(),
    'PORT': env.number().integer(),
    'DEBUG': env.boolean(),
  });
}
Enter fullscreen mode Exit fullscreen mode

In the background, the first step is to load the environment variables from the Platform.environment source (with the option of ignoring them) and/or from an environment file on disk if one exists.

As explained above, the package introduces a new DART_ENV variable which will tell us the environment in which our application will be running.

For example, when our environment is production, the package will automatically look at your project to find the .env.production file. If this does not exist, the .env file will be used instead.

If the DART_ENV variable is not defined, it will be assumed as the development value.

Secondly, we will validate our previously extracted data using our validation schemas in order to obtain an error in the event of non-compliance.

We can now retrieve our variables using our environment key.

final host = env.get('HOST');
print(host); // 127.0.0.1
Enter fullscreen mode Exit fullscreen mode

Existence security

In our previous example, we declared a string as a key in our validation scheme and then used another string to retrieve our value from our environment using the env.get(key) method.

To make the existence of our environment keys contractual, the package provides an abstract class called DefineEnvironment which you can implement in one of your classes.

Consider the following environment.

PORT=8080
HOST=localhost
URI={HOST}:{PORT}
Enter fullscreen mode Exit fullscreen mode

Let's define our environment using the abstract class DefineEnvironment.

final class Env implements DefineEnvironment {
  static final String host = 'HOST';
  static final String port = 'PORT';
  static final String uri = 'URI';

  @override
  final Map<String, EnvSchema> schema = {
    host: env.string().optional(),
    port: env.number().integer(),
    uri: env.string(),
  };
}
Enter fullscreen mode Exit fullscreen mode

We can now use our class to define our environment.

void main() {
  env.defineOf(Env.new);
  expect(env.get(Env.uri), 'localhost:8080');
}
Enter fullscreen mode Exit fullscreen mode

It is important to note that the result of the get method is an dynamic type, however this type is persisted during the validation of your environment variables, so its type is defined before accessing it.

So when you perform a get passing the PORT key, the return type will already be integer thanks to the prior validation process.

final port = env.get('PORT');
print(port.runtimeType); // integer
Enter fullscreen mode Exit fullscreen mode

🚧 Error handling

When your application starts and your environment does not meet the defined requirements by the validator, an EnvGuardException is thrown on the following format.

HOST=127.0.0.1
PORT=8080
LOG_LEVEL=trace
Enter fullscreen mode Exit fullscreen mode
enum MyEnum implements Enumerable<String> {
  info('info'),
  error('error'),
  debug('debug');

  @override
  final String value;

  const MyEnum(this.value);
}

env.define({
  'HOST': env.string(),
  'PORT': env.number().integer(),
  'LOG_LEVEL': env.enumerable(MyEnum.values)
});
Enter fullscreen mode Exit fullscreen mode

Since the value of our LOG_LEVEL key is not included in our enumerable, an error is raised.

{
  "errors": [
    {
      "message": "The value must match one of the expected enum values [info, error, debug]",
      "rule": "enum",
      "key": "LOG_LEVEL"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

❤️ Credits

My warmest thanks to Pierre Miniggio for proofreading and helping with the translation of this article.

View on Github View on Dart Pub

Top comments (0)