process-env-parser
Straightforward and type-safe environment variable validation, parsing, and
debugging for node
applications.
const result = parseEnvironmentVariables({
API_KEY: { mask: true, default: null },
DATABASE_URL: { parser: s => new URL(s), mask: Mask.url("password") },
LISTEN_PORT: { parser: parseInt, default: 3000 },
SERVICE_NAME: {}
});
if (result.success) {
console.table(result.envPrintable);
return result.env;
} else {
console.log(Formatter.multiLine(result));
throw new Error("Could not parse environment variables");
}
Rationale
At the start of every process there are two sources of inputs that can affect
the process execution: the program arguments, and the environment variables.
$ ENV_VAR_A=Hello ENV_VAR_B=World node app.js arg1 arg2 --arg3
In order to build reliable software, and minimize runtime surprises, you'll
want to follow the fail-fast design
and ensure that your program inputs are correct as early on as possible.
Everything the program does afterwards is be based on these inputs.
For example, ensuring that a required database URL is correctly passed to the
process at the very beginning will alert the user clearly of a possible issue,
instead of the the app crashing 30 minutes later when the database connection
is done the first time.
This library tries to provide useful tooling for handling the environment
variable part of startup.
Installation and usage
$ npm install --save @absxn/process-env-parser
import {
parseEnvironmentVariables,
requireEnvironmentVariables
} from "@absxn/process-env-parser";
Both functions return the same Success | Fail
object:
type Success = {
success: true;
env: {
[variableName: string]:
| InferredParserFunctionReturnType
| InferredDefaultValueType
| string;
};
envPrintable: {
[variableName: string]: string;
};
};
type Fail = {
success: false;
envPrintable: { [variableName: string]: string };
};
Examples
Success: Simple usage with mandatory variables
Easiest way to read the variables is to use
requireEnvironmentVariables(...variableNames: string[])
. It reads given
variables, must find them all, and returns their values as strings.
To succeed, all listed variables must exist in the environment
Process startup
$ A=hello B=world node app
Code
const result = requireEnvironmentVariables("A", "B");
if (result.success) {
console.table(result.envPrintable);
return result.env;
} else {
}
Success: Optional and parsed variables
If you have a more complex setup for the variables, you can use
parseEnvironmentVariables(config: Config)
. This allows you to handle each
variable individually with additional functionality.
The config
object has variable names as keys, and the value is an object
specifying how to handle that variable.
The available options are:
interface Config {
[variableName: string]: {
default?: any;
parser?: (value: string) => any;
mask?: boolean | (value: any) => string;
};
}
To succeed:
- All varibales with no
default
given must exist in the environment
- Empty string
""
is considered as non-existing!
- No
parser
may throw
- Parser exceptions turn result into
Fail
and the exception message is
captured in the envPrintable
fields. See examples below.
Default value is used as is, also when parser is given, i.e. default value is
not passed to parser when used.
Process startup
$ REQUIRED=value PARSED=12345 node app
Code
function parser(s: string): number {
const p = parseInt(s);
if (isNaN(p)) {
throw new Error("Not a number");
} else {
return p;
}
}
const result = parseEnvironmentVariables({
REQUIRED: {},
PARSED: { parser },
OPTIONAL: { default: "OPTIONAL" }
});
if (result.success) {
console.table(result.envPrintable);
return result.env;
} else {
}
Fail: Variable missing
Process startup
$ VAR_A=value VAR_B= VAR_C="${X} ${Y} ${Z}" node app
WARNING β Special cases for "meaningless" strings:
- Empty string:
VAR_B
is also considered as missing. I.e. process.env.VAR_B
does exist, but the parser considers ""
equal to not set.
- Blank string:
VAR_C
is also considered not set. In this case, X
, Y
, Z
are all ""
, so the resulting value of VAR_C
is two spaces,
" "
. If value is surrounded by spaces, e.g. " A "
, the spaces are
preserved as is through the parser.
Code
const result = requireEnvironmentVariables("VAR_A", "VAR_B", "VAR_C", "VAR_D");
if (result.success) {
} else {
console.table(result.envPrintable);
}
Fail: Parser throwing
Process startup
$ NOT_ACTUAL_NUMBER=xyz node app
Code
function parser(s: string): number {
const p = parseInt(s);
if (isNaN(p)) {
throw new Error("Not a number");
} else {
return p;
}
}
const result = parseEnvironmentVariables({
NOT_ACTUAL_NUMBER: { parser }
});
if (result.success) {
} else {
console.table(result.envPrintable);
}
Mask
Helpers for masking parts of variables for output.
import { Mask } from "@absxn/process-env-parser";
url()
A function that returns a function that applies the mask to given URL parts.
Valid URL parts are "hash"
, "hostname"
, "password"
, "pathname"
,
"port"
, "protocol"
, "search"
, and "username"
. Can handle both URL
strings and URL objects (from parser
or default
).
const result = parseEnvironmentVariables({
API_URL: { parser: s => new URL(s), mask: Mask.url("password", "path") }
});
For API_URL=https://user:pass@1.2.3.4/api/path
, the envPrintable
would
contain { API_URL: "https://user:*****@1.2.3.4/*****" }
.
urlPassword()
Same as url("password")
, resulting in
"protocol://user:*****@hostname/api/path"
urlUsernameAndPassword()
Same as url("username", "password")
, resulting in
"protocol://*****:*****@hostname/api/path"
.
Combine
Helpers for manipulating parser results.
import { Combine } from "@absxn/process-env-parser";
Non-nullable
If you have a subset of environment variables that depend on each other, i.e.
you either need all of them, or none of them, this function helps to ensure
that.
"Nullable" is here defined by TypeScript's NonNullable<T>
, that is, null
or
undefined
.
Lets assume we have this setup:
function getConfig() {
const result = parseEnvironmentVariables({
DATABASE: {},
USERNAME: { default: null },
PASSWORD: { default: null }
});
if (!result.success) {
return null;
}
const { DATABASE, USERNAME, PASSWORD } = result.env;
return {
auth: Combine.nonNullable({ USERNAME, PASSWORD }),
db: DATABASE
};
}
We would get the following results with given startup parameters:
$ DATABASE=db USERNAME=user PASSWORD=pass node app
getConfig() -> { auth: { USERNAME: "user", PASSWORD: "pass" }, db: "db" }
$ DATABASE=db node app
getConfig() -> { auth: null, db: "db" }
$ DATABASE=db USERNAME=user node app
getConfig() -> new Error("Mix of non-nullable (USERNAME) and nullable (PASSWORD) values")
$ node app
getConfig() -> null
If the object is returned, the return type has nullability removed from each
value:
const nullableValues = {
a: Math.random() > 0.5 ? "X" : null,
b: Math.random() > 0.5 ? 1 : undefined
};
const nonNullableValues = Combine.nonNullable(nullableTypes);
Formatter
The library contains additional helper functions for printing out the parser
results. These can be useful for storing the startup configuration into logs
or printing out startup failure reasons.
Importing Formatter
from the package:
import { Formatter } from "@absxn/process-env-parser";
console.table()
As a built-in, console.table()
is the easiest way to get a readable dump from
the parser results.
const result = requireEnvironmentVariables("VARIABLE");
console.table(result.envPrintable);
Oneliner
Using the data from the first example:
const result = parseEnvironmentVariables({
API_KEY: { mask: true, default: null },
DATABASE_URL: { parser: s => new URL(s).toString() },
LISTEN_PORT: { parser: parseInt, default: 3000 },
SERVICE_NAME: {}
});
console.log(Formatter.oneliner(result));
Multi-line
Output using same data as above example:
console.log(Formatter.multiLine(result));