React Environment Variables & NPM Version Locking

A React application can work perfectly on one developer’s laptop and fail in deployment for two unrelated reasons: it was built with the wrong configuration, or it was installed with a different dependency tree. Both problems look like “an environment issue,” but they require different fixes.
Environment variables tell your application how to behave. Dependency declarations and lockfiles determine which packages it uses. Node.js and npm versions define the tools that install and build those packages. Keeping these responsibilities separate makes releases easier to reproduce and failures easier to diagnose.
This guide uses React tooling and npm behavior available in 2019; tools and interfaces may have changed since then.
We’ll use Create React App for the frontend examples and npm 6 for dependency management. React itself does not prescribe an environment-variable system, so projects with custom webpack configurations may behave differently.
First, separate configuration from version management
There are no standard Node.js global variables named npm_version or npm_revision_id that control dependency versions. Setting either name will not select a Node.js release, upgrade npm, or unlock a package.
Instead, there are four separate controls:
- Application environment variables: configuration such as an API base URL.
- Node.js version: the JavaScript runtime used by your build tools or backend.
- npm version: the package manager used to resolve and install dependencies.
- Dependency versions: package requirements in
package.json, with a resolved dependency tree recorded inpackage-lock.json.
Check your installed tools directly:
node --version
npm --version
For a running Node.js process, process.version reports the runtime version, while process.env provides environment variables. These are not interchangeable with package version declarations.
Upgrading npm does not inherently require changing every dependency. Compatibility depends on the packages, their runtime requirements, and the package manager’s behavior, not a shared numbering scheme between Node.js, npm, and Express.
How React environment variables work
Browser JavaScript does not have direct access to your server’s operating-system environment. A React build tool must explicitly make selected values available to the frontend.
In Create React App, custom environment variables beginning with REACT_APP_ are substituted into the application during development or production compilation.
const apiBaseUrl = process.env.REACT_APP_API_BASE_URL;
In the browser bundle, this becomes a value embedded by the build process. It is not a live lookup against the deployment server’s environment.
This distinction has two important consequences:
- Changing a build-time variable requires rebuilding the frontend.
- Any value included in frontend code must be treated as public.
Never put secrets in frontend variables
A prefix is an exposure rule, not a security boundary. Anyone who downloads your application can inspect its JavaScript and network requests.
An API base URL is appropriate frontend configuration. A database password, private signing key, or privileged API token is not.
Keep secrets in a backend service. Let the frontend call that service through an appropriately authenticated endpoint rather than embedding server credentials in a bundle.
Configure a Create React App project step by step
1. Add a local environment file
Create .env.local in the project root, next to package.json:
REACT_APP_API_BASE_URL=http://localhost:4000
REACT_APP_ENABLE_REPORTS=false
Read those values from a small configuration module:
// src/config.js
const apiBaseUrl = process.env.REACT_APP_API_BASE_URL;
if (!apiBaseUrl) {
throw new Error('REACT_APP_API_BASE_URL is required');
}
export const config = {
apiBaseUrl: apiBaseUrl.replace(/\/+$/, ''),
reportsEnabled: process.env.REACT_APP_ENABLE_REPORTS === 'true'
};
Environment values are strings. Comparing explicitly with 'true' avoids a common bug: the string 'false' is truthy in JavaScript.
This validation runs when the module executes. If you need missing configuration to stop a CI build itself, add a separate prebuild validation script; a runtime check alone does not guarantee that compilation fails.
2. Document the required values
Commit a template with safe examples:
# .env.example
REACT_APP_API_BASE_URL=http://localhost:4000
REACT_APP_ENABLE_REPORTS=false
Create React App does not automatically load .env.example. Its purpose is to show developers which variables to provide.
Keep local overrides out of version control:
.env.local
.env.development.local
.env.test.local
.env.production.local
A shared .env file can be committed if its contents are intentionally public and suitable as defaults. Regardless of filename, never commit secrets or expose them through frontend variables.
3. Understand file precedence
For Create React App, environment files are checked in this order, from highest to lowest priority:
| Command | Files |
| --- | --- |
| npm start | .env.development.local, .env.local, .env.development, .env |
| npm test | .env.test.local, .env.test, .env |
| npm run build | .env.production.local, .env.local, .env.production, .env |
Variables already present in the shell environment take precedence over these files. Notice that tests skip .env.local; this reduces accidental dependence on a developer’s personal configuration.
Restart the development server after editing environment files. Create React App reads them when its tooling starts, not on every browser request.
4. Supply production values during the build
On a Unix-like shell, you can set a variable for a single command:
REACT_APP_API_BASE_URL=https://api.example.com npm run build
That inline syntax is not portable to every Windows shell. For cross-platform workflows, use environment files, your CI system’s environment settings, or a tool such as cross-env.
Create React App sets NODE_ENV according to the command: development for start, test for test, and production for build. You cannot override it through the normal configuration mechanism.
For a staging deployment, still make a production build. Use a separate variable such as REACT_APP_DEPLOYMENT_ENV=staging if your code needs that label.
5. Decide whether configuration must change after deployment
Changing an environment variable on a server that only serves an existing React build does not modify that build. You must rebuild and redeploy it.
If the same artifact must run in several environments, design a runtime configuration mechanism instead. For example, fetch a public /config.json before rendering the application. That requires deliberate handling of loading, validation, caching, and failures; it is not supplied automatically by Create React App’s build-time variables.
What npm version locking actually means
The dependencies section in package.json describes acceptable package versions. It does not specify your npm version.
Consider these declarations:
{
"dependencies": {
"react": "^16.8.6",
"react-dom": "^16.8.6"
}
}
Common version specifications have different meanings:
| Specification | Meaning |
| --- | --- |
| 16.8.6 | Exactly that version |
| ~16.8.6 | Patch releases from 16.8.6 up to, but not including, 16.9.0 |
| ^16.8.6 | Releases from 16.8.6 up to, but not including, 17.0.0 |
| 16.x | Any matching version in major version 16 |
| * | A broad range with no useful major-version boundary |
| latest | The version selected by the package’s latest distribution tag |
Caret ranges behave more conservatively for zero-major versions. For example, ^0.2.3 permits versions below 0.3.0, not every future 0.x release.
Ranges express an update policy. They do not guarantee that every matching release works with your application. Semantic versioning is a compatibility convention, not a substitute for tests.
Likewise, changing Express from 3.4.x to 3.x broadens the permitted minor versions; it does not solve a Node.js or npm compatibility problem.
Lock dependencies without freezing development
The answer is not “never depend on a specific version.” For applications, predictable installation is usually a feature.
A useful workflow combines intentional version ranges, a committed lockfile, and reviewed updates.
1. Generate and commit the lockfile
Run:
npm install
With npm 6, this creates or updates package-lock.json. The lockfile records the resolved dependency tree, including transitive dependencies and, where applicable, download locations and integrity information.
Commit both manifests:
git add package.json package-lock.json
git commit -m "Record application dependency tree"
Pinning only top-level packages is not enough to fix every nested dependency. The lockfile captures those resolutions too.
2. Use clean installs in CI
Use npm ci, available in npm 6, for automated builds:
npm ci
CI=true npm test -- --watchAll=false
npm run build
npm ci requires a lockfile, removes an existing node_modules directory, and installs the recorded dependency tree. It fails if the lockfile and package.json disagree rather than silently updating the lockfile.
It improves dependency reproducibility, but it does not make every build identical. Operating systems, native modules, installation scripts, and external resources can still affect results.
3. Update intentionally
Inspect available updates:
npm outdated
Then update selected packages, run tests, and review the manifest and lockfile changes. For an intentional exact-version update using a release available in 2019:
npm install --save-exact react@16.9.0 react-dom@16.9.0
Avoid deleting the lockfile as a routine troubleshooting step. Regenerating it can change many unrelated transitive packages at once, making regressions harder to isolate.
Align Node.js and npm across the team
A lockfile does not select your runtime or package manager. Record those separately.
For example, a late-2019 project could use an .nvmrc file:
12.13.0
Developers using nvm can then run:
nvm install
nvm use
Document the expected npm version and verify it in CI. An engines declaration can communicate supported versions:
{
"engines": {
"node": "12.x",
"npm": "6.x"
}
}
Under typical npm 6 settings, engine mismatches produce warnings rather than enforcing a toolchain. Configure CI explicitly instead of treating engines as an automatic version switcher.
Common pitfalls and a short takeaway
When troubleshooting, first classify the failure:
- Undefined frontend value: check the
REACT_APP_prefix, file location, and dev-server restart. - Production still uses an old URL: rebuild the bundle with the intended configuration.
- Local and CI installs differ: check the committed lockfile, install command, and tool versions.
- Unexpected updates: inspect version ranges and lockfile changes.
- A credential appears in JavaScript: remove it from frontend configuration and rotate the exposed credential.
The practical takeaway is simple: configure React at the correct stage, keep secrets on the server, commit your dependency lockfile, and build with a consistent toolchain. Environment variables control application behavior; version ranges define allowed updates; lockfiles record what you actually install.
Conversation (0)
No comments yet. Start the conversation.
Join the conversation
Create a free member account to comment.