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

defaulted

v1.0.2

Published

Manage config from environment variables with sensible defaults, with typing and validation.

Downloads

45

Readme

defaulted

npm package typescript MIT license test status

defaulted() constructs a configuration object from given default values and the process environment. It allows for defining sets of defaults for different capital-E ENVIRONMENTs. Keys and values are typed and validated. The config will fail loudly if you forget to define a value and try to use it, or set a defined value to the wrong type. The environment groups are useful for ensuring certain configs are always set a certain way, or must always be defined, for particular deployment contexts.

Installation

npm install defaulted

and include as a JavaScript or TypeScript module (types included):

import defaulted from "defaulted";

…or a CommonJS module:

const defaulted = require("defaulted");

Usage

Set up centralized config with defaults that may be overridden by environment variables. The defaults act like a schema. Only keys present in the defaults will be extracted from the environment, and they will be cast to a type matching the type of the default value.

const config = defaulted({
  DATABASE_URL: "postgres://localhost:5432/db",
  NEW_FEATURE_ENABLED: true,
  SERVICE_URL: "https://sandbox-api.example.com",
});

Read config values:

const client = new DBClient(config.DATABASE_URL);

if (config.NEW_FEATURE_ENABLED) {
  console.log("Enabled!");
} else {
  console.log("Not Enabled!");
}

const response = await fetch(config.SERVICE_URL);

Override values via env:

> NEW_FEATURE_ENABLED=false node main.js
Not Enabled!

Trying to access config keys not defined or set any will throw:

if (config.OTHER_FEATURE) { // <-- Throws!
config.NEW_FEATURE_ENABLED = false // <-- Throws!

Environment Overrides

defaulted() takes one or two arguments: the default values to use if the matching variables are not defined in the environment, and an optional mapping of overrides. These overrides will be selected based on value of the ENVIRONMENT variable.

For another example, given this setup:

const config = defaulted({
  DATABASE_URL: "postgres://localhost:5432/db",
  MOCK_EMAIL: true,
  SMTP_HOST: "smtp.example.com",
  SMTP_PORT: 587,
}, {
  prod: {
    DATABASE_URL: undefined,
    MOCK_EMAIL: false,
  },
  test: {
    MOCK_EMAIL: false,
    SMTP_HOST: "smtp-echo.local"
  },
});

If you do want to send email from your local machine, you can disable the mocking just for that "deployment" by starting the server with something like MOCK_EMAIL=false npm start. Conversely, a deployment with ENVIRONMENT=prod will not mock email by default unless overridden by the environment. The prod environment also requires the DATABASE_URL be defined, while local dev doesn't need any additional configuration out of the box.

Validation

defaulted attempts to be strict about the keys and values, to avoid issues from failing to define a necessary value or inconsistencies from modifying the values after boot.

The resulting config object will have explicit keys matching the default, to aide with type checking and autocomplete. Attempting to access a key that was not specified will throw. Those keys will also be read-only, and will likewise throw on assignment. (Doing either will also fail type checking.)

If the environment specifies a value that cannot be cast to the type, defaulted will throw. See below for specifics on acceptable values.

Empty strings

If the default type is a string, empty strings will be allowed in the environment. For numbers and booleans, empty strings present will cause defaulted to throw. These keys in the environment must be set to a non-empty value, or unset.

Numbers

Keys with number-type defaults must be numeric in the environment if present. Any value that results in NaN when converted will throw. Integers, floats, hexadecimal, binary, and exponential are allowed.

Note that process.env provides strings only, so the casting behavior of Number might not work as expected. Given VAL=true, Number(process.env.VAL) will produce NaN rather than 1 the way Number(true) does. For this reason, defaulted will throw instead.

Booleans

Booleans must be specified in the environment using the words "true" or "false", in any capitalization, or the numbers "1" or "0". Any other value will throw if the default is expecting a boolean. Note that, like with numbers, this different than Boolean() casting since process.env only provides strings.

defaulted.secrets()

defaulted works well for secrets and allows you to provide non-sensitive defaults locally, but set explicitly undefined values for production. This guards against deploying a new instance of an app without mandatory secret config. This is possible with the regular defaulted, but gets repetitive. The defaulted.secrets(…) function makes this easier by inverting the approach to merging the defaults.

const secrets = defaulted.secrets([
  "SESSION_SECRET",
  "SOME_API_KEY",
], {
  local: {
    SESSION_SECRET: "dev-session-secret",
  },
  test: {
    SESSION_SECRET: "test-session-secret",
    SOME_API_KEY: "mocked",
  },
});

When calling defaulted.secrets, specified keys do not have default values unless defined in the environment overrides, and are required to be provided by env vars. The function will throw if they are not found in process.env. This way, local development or tests can have out-of-the-box unsensitive defaults if available, but production must provide the secrets via env variables.

Like regular defaulted-based configs, accessing keys not specified in the initial list, trying to set a key, or trying to override an unspecified key will throw. These operations will also fail type checking but make sure the list of keys is marked as const for thorough strictness.

const secrets = defaulted.secrets([
  "SESSION_SECRET",
  "SOME_API_KEY",
] as const);

Note that secret values are passed through as strings. If for some reason you do need Number or Boolean secrets, you can use the regular defaulted call and set the env overrides to undefined to make them mandatory:

const secrets = defaulted({
  SECRET_NUMBER: 4,
}, {
  prod: {
    SECRET_NUMBER: undefined,
  },
});

The above will throw in ENVIRONMENT=prod if SECRET_NUMBER is not in the env.

FAQ & Best Practices

Secrets

A useful way to handle secrets is create an explicit secrets config. This separates secret values from the regular config so they are clearly sensitive and it's easy to trace which code uses them.

const config = defaulted({
  SOME_API_URL: "https://sandbox-api.example.com",
}, {
  prod: {
    SOME_API_URL: "https://api.example.com",
  },
});

const secrets = defaulted.secrets([
  "DATABASE_URL",
  "SOME_API_KEY",
], {
  local: {
    DATABASE_URL: "postgres://localhost:5432/db",
    SOME_API_KEY: "sandbox12345",
  },
  test: {
    SOME_API_KEY: "mocked",
  },
  stag: {
    SOME_API_KEY: "sandbox12345",
  },
  prod: {
  },
});

export default config;
export { secrets };

With this setup, it’s easy to trace the usage of any particular config, and especially the secrets:

import config, { secrets } from "./config";
import APIClient from "some-api";

export function getAPIClient () {
  return new APIClient(config.SOME_API_URL, {
    token: secrets.SOME_API_KEY,
  });
}

ENVIRONMENT names

Name the ENVIRONMENTS by their primary functional difference, rather than a specific stage, app, or instance names. Some suggestions:

  • "prod" is for live production instances, ones where it matters when things go wrong
  • "dev" is any non-production deployment, be it Staging, QA, UAT, SIT, or any other name or acronym, potentially even locally on a developer machine (there could be any number of deployments or stages in a pipeline using this ENVIRONMENT)
  • "test" is any CI or similar test context that needs specific changes for mocking or instrumentation
  • "local" is a good alternative to "dev" when perhaps certain infrastructure pieces like queues need to be simulated

If there are more than a few ENVIRONMENTs, that’s a hint the config needs a simplification pass. Likewise if each environment needs to set a lot of variables, reconsider how different the environments need to be from each other.

Note that different deployments can share an ENVIRONMENT but set different values in their specific instance. The EVIRONMENT is just a label that selects a set of config defaults and expectations as defined in the code.

12 Factor Config

I thought the 12 Factor App said to store config in the environment.

It does, and it’s right! defaulted is more precisely a way to manage the defaults, and ensure consistency. It seeks a balance between the purity of env-based config with the practicality of consistent groupings with centralized declarative defaults.

In practice, unless you’re always doing it live there usually are at least two classes of config: production and non-production. External tools can help manage this, but apps should own their expectations of configuration while allowing for explicit overrides on a per-deployment basis.

Why not NODE_ENV?

Apps and tooling have different interpretations of NODE_ENV, some only allowing specific values. The consensus seems to be it refers to a build-time performance configuration rather than a deployment context. When NODE_ENV=production, bundles are minified, debug instrumentation disabled, dev dependencies omitted etc. Those changes are important for production, but they’re also valuable for any non-prod environment where you want to test code that is as close to the actual prod version as possible. They’re also necessary to practice stateless builds that can be promoted to production. Besides, NODE_ENV is node-specific, whereas ENVIRONMENT does not imply a particular technology, so other services can respond to it without confusion.

Nesting or lists

Storing more complex values in the env beyond a singular token is not recommended. However, you are free to use regular strings for that and parse them yourself. defaulted doesn’t support validating such values because the schemas quickly become complicated or app/technology-specific, and are better handled by the app itself if they are absolutely necessary. Odds are they are better suited as data rather than config.

Key names

To maximize compatibility, and reinforce the notion that these values should be constant for the life of the process, use uppercase with underscores, eg FEATURE_FLAG.

Any valid object property name is allowed for defaults. This includes mixed cases, spaces, and non-alphanumeric characters. That said, spaces are disallowed in most POSIX environments, and some even disallow lowercase. Others are case-insensitive. Even if there is a default defined, the app may not be able to read some key from the environment.

Author

Alec Perkins

License

This package is licensed under the MIT License.

See ./LICENSE for more information.