← Node & Express

Node Fundamentals

What are environment variables in Node.js, how do you read them, and why should you not hardcode secrets in the code?

Show answer — try answering out loud first

Environment variables are configuration values that live outside the code, in the environment where the application runs. In Node they are read with process.env. In development they are usually stored in a .env file loaded with the dotenv library. You should not hardcode secrets (passwords, API keys) in the code because they end up in version control, get exposed, and are hard to change between environments.

An application needs configuration that changes depending on where it runs: the port, the database URL, the keys for external services. That configuration should not be written in the code, because:

  • It changes between environments (development, testing, production).
  • Some values are secrets (passwords, tokens) and should not stay in the repository.

The solution is environment variables: values that the operating system (or the deployment platform) passes to your process. In Node you read them through the process.env object, which is an object with all the available variables.

The .env file and dotenv: in development it's inconvenient to type the variables by hand in the terminal every time. That's why a file called .env is used with KEY=value pairs, along with the dotenv library, which reads that file and puts its values into process.env. Very important: the .env file must be in .gitignore so it is never uploaded to the repository.

Why you should NOT hardcode secrets:

  • If you write a password or API key directly in the code, it stays in the Git history forever, even if you delete it later.
  • Anyone with access to the repository (or if the repo is leaked) sees your credentials.
  • You cannot use different values per environment without editing the code.
  • Rotating (changing) a secret forces you to modify and deploy code.

Best practices:

  • All secrets and configuration go in environment variables.
  • Always use a reasonable default value where it makes sense (for example the port), so the app starts the same way locally.
  • Push a sample .env.example file to the repo (without real values) that documents which variables are needed.
  • Remember that everything in process.env is text (string): if you need a number, convert it with Number().
// Load the variables from the .env file into process.env.
// (It must go as high up as possible, before using the variables.)
require("dotenv").config();

// Read the port with a default value: if PORT is not defined, use 3000.
const PORT = process.env.PORT || 3000;

// A secret: it is NEVER written here directly, it is read from the environment.
const API_KEY = process.env.API_KEY;

if (!API_KEY) {
  // Fail early and clearly if a critical variable is missing.
  throw new Error("Falta la variable de entorno API_KEY");
}

// process.env always gives strings; if you need a number, convert it:
const MAX_CONEXIONES = Number(process.env.MAX_CONEXIONES) || 10;

console.log(`Servidor arrancara en el puerto ${PORT}`);
console.log(`Maximo de conexiones: ${MAX_CONEXIONES}`);
# file: .env  (this file goes in .gitignore, it is NEVER uploaded)
PORT=8080
API_KEY=sk_live_ejemplo_no_real_123
MAX_CONEXIONES=25
# file: .env.example  (this one IS uploaded, without real values)
PORT=
API_KEY=
MAX_CONEXIONES=

Note: as of Node.js 20 there is the node --env-file=.env app.js flag, which loads a .env file without needing the dotenv library. Even so, dotenv is still the most common and compatible option.

Writing the secret directly in the code "just for now" (const API_KEY = "sk_live_abc123") and pushing it to the repository. Even if you delete it later, it stays in the Git history and must be considered compromised: you have to rotate (change) that credential. Another frequent mistake is forgetting to put .env in .gitignore, which ends up publishing the file with the secrets.

"Environment variables are the configuration that lives outside the code, in the environment where the app runs, and in Node I read them with process.env. In development I store them in a .env file that I load with dotenv, and I put that file in gitignore so it never reaches the repo. I don't hardcode secrets because they would stay in the Git history forever, exposed, and it would be a hassle to change them between environments. I always use default values where it makes sense, for example process.env.PORT with a fallback to 3000, I validate critical variables early, and I remember that everything in process.env is a string, so I convert to a number when needed."

Quick challenge

What does this code print if the environment variable PORT is not defined?

const PORT = process.env.PORT || 3000;
console.log(typeof PORT, PORT);

And if you instead run with PORT=8080 node app.js, what does it print?

See answer

Without PORT defined it prints: number 3000. Since process.env.PORT is undefined, the || operator uses the default value 3000, which is a numeric literal.

With PORT=8080 node app.js it prints: string 8080. Now process.env.PORT does exist, but since everything in process.env is text, its type is string, not number. That's why, if you're going to do numeric operations, it's best to convert it with Number(process.env.PORT).