Environments
Define separate environments
Defining separate environments is important in the software development process because it allows developers to develop and test their software in a controlled environment that is separate from the live production environment. This helps to prevent issues such as bugs or errors from affecting the live production environment, which can ultimately improve the overall quality and reliability of the software.
Here is a list of the most common environments and their description. At a minimum, you should have a local, develop, and a production environment.
- local: Is set up on a developer's individual computer. This is where developers can work on their code and test it locally before sharing it with other members of the development team.
- develop: Is where the software is developed and tested by the development team. This is typically a shared environment where multiple developers can work on the software simultaneously.
- test: Is where the software is tested to ensure that it functions correctly and meets the specified requirements. This is typically a controlled environment that is separate from the development and production environments, and is used to simulate the conditions of a production environment.
- staging: Is where the software is tested in a production-like environment before it is deployed to the live production environment. This allows developers to ensure that the software is ready for deployment and to identify and fix any potential issues before the software is made available to users.
- production: Is the live environment where the software is deployed and used by end users. This is the final stage of the software development process, and it is important to ensure that the software is stable and reliable before it is deployed to the production environment.
Support Windows and POSIX on development scripts
Although your solution is probably designed to be run in a POSIX environment in production, make sure that all your development scripts (e.g. build
, test
, lint
, start
) are Windows and POSIX compatible.
Windows and POSIX are two different types of operating systems, with their own sets of commands and tools. Windows is a proprietary operating system developed by Microsoft, while POSIX is a standard for operating systems based on the Unix family of operating systems, such as Linux and macOS.
By supporting both OS on development scripts, users can use the tools and processes that they are most familiar with, which can make the development process more efficient and user-friendly.
For example, the following package.json file defines a script called build that runs a webpack command to build the project:
{
"name": "my-project",
"version": "1.0.0",
"scripts": {
"build": "webpack"
}
}
To make this script cross-compatible on both Windows and POSIX systems, you can use the cross-env (opens in a new tab) package to set environment variables that are required for the script to run correctly.
For example, the following package.json file defines a script called build that uses the cross-env package to set the NODE_ENV
environment variable to production before running the webpack command:
{
"name": "my-project",
"version": "1.0.0",
"scripts": {
"build": "cross-env NODE_ENV=production webpack"
},
"devDependencies": {
"cross-env": "^7.0.2"
}
}
Load environment-specific configurations from environment variables
Loading environment-specific configurations from environment variables can help improve security by keeping sensitive information out of the codebase, and allows for flexibility in the configuration of the application as the same codebase can be used in different environments without modifying the code.
Use .env files to store your local environment variables
The .env file is a simple text file that contains environment-specific configuration settings, such as database connection strings or API keys. The .env file is typically used in local environment to store configuration settings that are specific to that environment.
To use the .env file, you can create one in the root directory of your project. Then, you can add configuration settings to the file in the form of key-value pairs, with each setting on a new line in the following format:
KEY=VALUE
For example, if you have a database connection string that you want to use in your application, you could add the following line to your .env file:
DATABASE_CONNECTION_STRING=mysql://user:password@host:port/database
Once you have added your configuration settings to the .env file and loaded them with the dotenv package (opens in a new tab), you can access them in your code by using the process.env
object in Node.js.
For example, if you want to access the DATABASE_CONNECTION_STRING
setting from the .env file in your code, you could use the following code:
const connectionString = process.env.DATABASE_CONNECTION_STRING;
It is important to note that the .env file should not be committed to version control, as it can contain sensitive information such as passwords or API keys. You should add the .env file to your .gitignore file to ensure that it is not committed to your code repository. This will prevent sensitive information from being exposed if the codebase is shared or made public. Instead commit a .env.example which serves as a guide for developers.
Validate environment variables before your app starts
Validating environment variables before your app starts can help to ensure that your application has the required configuration settings and that they have the correct values, which can improve reliability, performance, and security.
Here is an example of how to validate environment variables using zod (opens in a new tab):
import { z } from "zod";
// Define the schema for the environment variables
const schema = z.object({
DATABASE_CONNECTION_STRING: z.string(),
API_KEY: z.string(),
});
// Validate the environment variables against the schema
const parsed = schema.safeParse({
DATABASE_CONNECTION_STRING: process.env.DATABASE_CONNECTION_STRING,
API_KEY: process.env.API_KEY,
});
// Check if the validation failed
if (parsed.success === false) {
// If the validation failed, print the error and exit the application
console.error(
"❌ Invalid environment variables:",
parsed.error.flatten().fieldErrors
);
process.exit(1);
}
// If the validation was successful, start the app
startApp();
Set the node and npm versions in the engines property of the package.json
Setting the node and npm versions in the engines property of the package.json ensures that the correct version of node and npm are being used and helps prevent potential issues that may arise from using incompatible versions.
Learn more (opens in a new tab)
Additionally, use nvm and create a .nvmrc in the project root
The nvm (Node Version Manager) (opens in a new tab) tool is a command-line utility that allows you to manage multiple versions of node on the same machine.
Using nvm and creating a .nvmrc in the project root allows developers to easily switch between different versions of node and ensures that all team members are using the same version, which can help prevent potential issues that may arise from using incompatible versions.
If your project use nvm, don't forget to mention it in the documentation.
Setup a preinstall script that checks node and npm versions
It can help to ensure that your project is running on a supported and compatible environment. This can prevent issues such as compatibility errors or performance problems that may occur if your project is running on an unsupported or incompatible version of node or npm.
Here is an example of how to set up a preinstall script that checks the node and npm versions using the semver (opens in a new tab) library in a script file:
// scripts/check-engines.js
const childProcess = require("child_process");
const semver = require("semver");
const packageJson = require("../package.json");
// Check if current versions are supported by the engines property
let supported = true;
if (packageJson.engines) {
if (packageJson.engines.node) {
const nodeVersion = process.version;
const supportedNodeVersion = packageJson.engines.node;
if (!semver.satisfies(nodeVersion, supportedNodeVersion)) {
console.log(
`Current Node.js version ${nodeVersion} is not supported by this project (requires ${supportedNodeVersion})`
);
supported = false;
}
}
if (packageJson.engines.npm) {
const npmVersion = childProcess.execSync("npm -v").toString().trim();
const supportedNpmVersion = packageJson.engines.npm;
if (!semver.satisfies(npmVersion, supportedNpmVersion)) {
console.log(
`Current npm version ${npmVersion} is not supported by this project (requires ${supportedNpmVersion})`
);
supported = false;
}
}
}
if (!supported) {
process.exit(1);
}
Then just call the previous script from the preinstall
script in the package.json file
// package.json
{
"name": "my-project",
"version": "1.0.0",
"scripts": {
"preinstall": "node scripts/check-engines",
"start": "node index.js"
},
"engines": {
"node": "16.14.2",
"npm": ">=7"
}
}
Use deterministic dependencies lock file
A deterministic dependencies lock file is used to ensure that the exact same dependencies are installed in a project every time it is built, which is useful for reproducibility and consistency in environments where different developers or systems may be building the project.
Depending on the package manager your are using on your project, the dependencies lock file have different names:
- npm:
package-lock.json
- yarn:
yarn.lock
- pnpm:
pnpm-lock.yaml
Once you have a dependencies lock file in your project, your package manager will use this file to install the exact same dependencies and versions that were installed on the original environment.
Prior to npm v7, the package-lock.json
file was not deterministic.
If your project use npm as its package manager, make sure to enforce the usage of npm v7 and later in the engines property of the package.json.
Require a specific package manager to be used on the project
Requiring the use of a specific package manager, everyone on the team can be sure that they are using the same tool to manage dependencies, which can help avoid conflicts and make it easier to work together on the project.
Here is an example of how to force a specific package manager to be used on the project using the preinstall script and the only-allow (opens in a new tab) library:
// npm
{
"scripts": {
"preinstall": "npx only-allow npm"
}
}
// yarn
{
"scripts": {
"preinstall": "npx only-allow yarn"
}
}
// pnpm
{
"scripts": {
"preinstall": "npx only-allow pnpm"
}
}
Preferably, use pnpm as the package manager for your project
To improve the developers experience and ease the project setup, you should consider using pnpm (opens in a new tab) as the package manager for your project.
- pnpm can manage the Node.js version: In pnpm, you can specify the Node.js version of the project using the
use-node-version
(opens in a new tab) in the .npmrc file. By doing so, pnpm will automatically install the specified version of Node.js and use it for runningpnpm run
commands or thepnpm node
command. That means, you won't have to use nvm nor create a .nvmrc file at the project root. - pnpm lock file is deterministic: With pnpm, the
pnpm-lock.yaml
file is already deterministic and always been. - No need to check the npm/node versions: Since pnpm lock file have always been deterministic and pnpm can take care of the Node.js version, you won't have to checks node and npm versions.
Avoid globally installed modules
Globally installed modules can create conflicts with other modules or project dependencies, and can make it difficult to manage the specific dependencies and versions required for different projects.
It is recommended to avoid globally installed modules and instead manage dependencies on a per-project basis using a tool such as npm or pip. This allows you to specify the exact dependencies and versions needed for each project, and makes it easier to manage and update them as needed.
Ideally, use Docker
Docker is a tool that allows you to package an application and its dependencies into a single container that can be easily deployed and run in different environments.
This can be useful for a variety of reasons, including simplifying the process of setting up and configuring a development environment, making it easier to deploy and run applications in different environments, and allowing you to isolate different applications and services from each other.