npm environment variables let you inject configuration into your project at install time, bypassing the need for manual file management and version control of sensitive data.

Let’s see this in action. Imagine you have a package.json that needs a database URL, but you don’t want to hardcode it or commit it to Git.

{
  "name": "my-app",
  "version": "1.0.0",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "config": {
    "databaseUrl": "mongodb://localhost:27017/mydb"
  }
}

Normally, npm config values are only available during npm operations themselves. But there’s a trick. You can access them in your application code by leveraging the process.env object, but you need to expose them first.

The core idea is to use npm’s preinstall or postinstall scripts to read values from your package.json (or other sources) and then set them as environment variables that your application can then access.

Consider a package.json with a config section:

{
  "name": "my-app",
  "version": "1.0.0",
  "scripts": {
    "start": "node index.js",
    "preinstall": "node ./scripts/set-env.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "config": {
    "databaseUrl": "mongodb://localhost:27017/mydb",
    "apiKey": "supersecretkey123"
  }
}

And a scripts/set-env.js file:

const fs = require('fs');
const path = require('path');

const packageJsonPath = path.join(__dirname, '../package.json');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));

if (packageJson.config) {
  for (const key in packageJson.config) {
    if (Object.hasOwnProperty.call(packageJson.config, key)) {
      // Only set if not already defined in the environment
      if (!process.env[`APP_${key.toUpperCase()}`]) {
        process.env[`APP_${key.toUpperCase()}`] = packageJson.config[key];
        console.log(`Setting APP_${key.toUpperCase()} environment variable.`);
      }
    }
  }
}

Now, when you run npm install, the preinstall script executes node ./scripts/set-env.js. This script reads package.json, finds the config block, and for each key (like databaseUrl and apiKey), it sets an environment variable prefixed with APP_ (e.g., APP_DATABASEURL, APP_APIKEY).

Your application code can then access these like any other environment variable:

// index.js
const express = require('express');
const app = express();

const dbUrl = process.env.APP_DATABASEURL;
const apiKey = process.env.APP_APIKEY;

console.log(`Database URL: ${dbUrl}`);
console.log(`API Key: ${apiKey}`);

app.get('/', (req, res) => {
  res.send(`DB: ${dbUrl}, Key: ${apiKey}`);
});

const port = 3000;
app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

When you run npm start, you’ll see the values logged to the console, demonstrating they were injected at install time. This is incredibly powerful for managing configurations that change between environments (development, staging, production) without modifying your codebase. You can also use this to inject secrets that are managed by external secret managers or CI/CD pipelines.

The preinstall script is crucial here because it runs before any dependencies are installed. This means that if any of your dependencies rely on these environment variables during their own installation (e.g., to fetch specific binaries or configure themselves), they will have access to them.

This approach effectively turns your package.json into a primary source of truth for configuration that can be applied universally across different environments by simply changing the environment variables available when npm install is run.

The real magic is that process.env is a global object in Node.js. When the preinstall script runs, it modifies the process.env object for the current Node.js process. All subsequent scripts executed by npm within that same install lifecycle, including your application’s start script if it’s run directly by npm (or if the installation happens before the application is started), will inherit these modified environment variables.

A common pitfall is forgetting to handle cases where an environment variable might already be set. The provided set-env.js script checks !process.env[...] to avoid overwriting existing variables, which is good practice, especially when running in CI/CD where the environment might be pre-populated.

The next logical step is to explore how to manage these environment variables across different deployment environments more systematically, perhaps using tools like dotenv in conjunction with this npm script approach, or by leveraging platform-specific environment variable injection mechanisms.

Want structured learning?

Take the full Npm course →