npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@absxn/process-env-parser

v1.1.1

Published

Straightforward and safe environment variable parser for bootstrapping node applications

Downloads

814

Readme

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) {
  // Sample success output
  console.table(result.envPrintable);
  // ┌──────────────┬─────────────────────────────────────────────────────┐
  // │   (index)    │                       Values                        │
  // ├──────────────┼─────────────────────────────────────────────────────┤
  // │   API_KEY    │                     '<masked>'                      │
  // │ DATABASE_URL │ '<masked: "mysql://user:*****@localhost:3306/app">' │
  // │ LISTEN_PORT  │                       '8080'                        │
  // │ SERVICE_NAME │                      '"app"'                        │
  // └──────────────┴─────────────────────────────────────────────────────┘

  // Inferred type for successfully parsed environment
  // {
  //   API_KEY: string | null
  //   DATABASE_URL: URL
  //   LISTEN_PORT: number
  //   SERVICE_NAME: string
  // }
  return result.env;
} else {
  // Sample formatted output
  console.log(Formatter.multiLine(result));
  // API_KEY = <masked>
  // DATABASE_URL = <parser: "Invalid URL: localhost">
  // LISTEN_PORT = 3000 (default)
  // SERVICE_NAME = <missing>

  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:

// Types roughly as follows, read code and inline documentation for details

type Success = {
  success: true;
  env: {
    [variableName: string]:
      | InferredParserFunctionReturnType // If `parser` option used
      | InferredDefaultValueType // If `default` option used
      | string; // No options used
  };
  envPrintable: {
    // Human readable results for logging and debugging
    // E.g. `ENV_VAR_A=<missing>, ENV_VAR_B="World", PASSWORD=<masked>`
    [variableName: string]: string;
  };
};

type Fail = {
  success: false;
  // Same as for Success
  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

// Type: Success | Fail
const result = requireEnvironmentVariables("A", "B");

if (result.success) {
  console.table(result.envPrintable);
  // ┌─────────┬───────────┐
  // │ (index) │  Values   │
  // ├─────────┼───────────┤
  // │    A    │ '"hello"' │
  // │    B    │ '"world"' │
  // └─────────┴───────────┘

  // Type: { A: string, B: string }
  // Value: { A: "hello", B: "world" }
  return result.env;
} else {
  // Wont get here since we gave both A and B in the startup
}

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]: {
    // If variable is not found, use this as its value. If `default` not
    // given, variable is mandatory, in which case, a missing variable leads
    // to Fail being returned. If default value was used, envPrintable will
    // have " (default)" appended to the printable value.
    default?: any;
    // When variable is read, its value is passed first to the parser
    // function. Return value of the parser is used as the variable value in
    // the output. If the parser throws, the function will return a Fail
    // object.
    parser?: (value: string) => any;
    // If `true`, the value of the variable is never shown in plain text in
    // the `envPrintable` fields of the return object. Value is indicated as
    // `<masked>`. If function, the argument is 1) return value of parser 2)
    // environment variable value. Default value bypasses the function and gets
    // displayed as `<masked> (default)`. Return value of the function is the
    // value to be shown in `envPrintable`, formatted as <masked: "value">.
    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

// Ensure we return only valid numbers
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);
  // ┌──────────┬────────────────────────┐
  // │ (index)  │         Values         │
  // ├──────────┼────────────────────────┤
  // │ REQUIRED │       '"value"'        │
  // │  PARSED  │         '1234'         │
  // │ OPTIONAL │ '"OPTIONAL" (default)' │
  // └──────────┴────────────────────────┘

  // Type: { REQUIRED: string, PARSER: number, OPTIONAL: "OPTIONAL" | string }
  // Value: { REQUIRED: "value", PARSED: 1234, OPTIONAL: "OPTIONAL" }
  return result.env;
} else {
  // Will not get here
}

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) {
  // Won't get there
} else {
  console.table(result.envPrintable);
  //  ┌─────────┬─────────────┐
  //  │ (index) │   Values    │
  //  ├─────────┼─────────────┤
  //  │  VAR_A  │  '"value"'  │
  //  │  VAR_B  │ '<missing>' │
  //  │  VAR_C  │ '<missing>' │
  //  │  VAR_D  │ '<missing>' │
  //  └─────────┴─────────────┘
}

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) {
  // Won't get there
} else {
  console.table(result.envPrintable);
  // ┌───────────────────┬────────────────────────────┐
  // │      (index)      │           Values           │
  // ├───────────────────┼────────────────────────────┤
  // │ NOT_ACTUAL_NUMBER │ '<parser: "Not a number">' │
  // └───────────────────┴────────────────────────────┘
}

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:[email protected]/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() {
  // For parsing purposes, both USERNAME and PASSWORD are optional...
  const result = parseEnvironmentVariables({
    DATABASE: {},
    USERNAME: { default: null },
    PASSWORD: { default: null }
  });

  if (!result.success) {
    return null;
  }

  const { DATABASE, USERNAME, PASSWORD } = result.env;

  return {
    // ... but for actual authentication, you need both
    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:

// Type before: { a: string | null, b: number | undefined }
const nullableValues = {
  a: Math.random() > 0.5 ? "X" : null,
  b: Math.random() > 0.5 ? 1 : undefined
};
// Type after: {a: string, b: number} | null
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);
// ┌──────────┬─────────┐
// │ (index)  │ Values  │
// ├──────────┼─────────┤
// │ VARIABLE │ 'value' │
// │   ...    │   ...   │
// └──────────┴─────────┘

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));
// if (result.success === true):
// > API_KEY=<masked>, DATABASE_URL="mysql://localhost:3306/app", LISTEN_PORT=8080, SERVICE_NAME="app"
// else:
// > API_KEY=<masked>, DATABASE_URL=<parser: "Invalid URL: localhost">, LISTEN_PORT=3000, SERVICE_NAME=<missing>

Multi-line

Output using same data as above example:

console.log(Formatter.multiLine(result));
// if (result.success === true):
// > API_KEY = <masked>
//   DATABASE_URL = "mysql://localhost:3306/app"
//   LISTEN_PORT = 8080
//   SERVICE_NAME = "app"
// else:
// > API_KEY = <masked> (default)
//   DATABASE_URL = <parser: "Invalid URL: localhost">
//   LISTEN_PORT = 3000 (default)
//   SERVICE_NAME = <missing>