The NPM guide I would have loved as a beginner

As a junior developer, I struggled a lot with NPM.
Do you remember copy/pasting all the npm commands without knowing what they do ? Or the first time you freaked out while opening the package-lock.json ? What if I tell you that there's a bugs property that you can set in your package.json ?

No more fear is allowed from that point onward, we're going to learn together the basics of NPM.

Disclaimer - The article will follow my process of re-learning from scracth. Feel free to skip to specific parts if you don't want to read what NPM means, etc.

Table of contents

A little background

Let's start with the basics, what does NPM even mean ? It stand for Node Package Manager, and as the name implies, it's responsible for managing your packages within your Node application.

Now considered as a major piece of Javascript ecosystem, NPM offer an easy way to manage all the external dependencies we'll need to use in our project with a pretty simple command : npm install <your_package>.

I will skip the installation of NPM, the Node website will explain it to you properly, and is not the core of this article.

I'll jump straight to the npm commands. Let's start.

Initialization

When I typed my first npm commands, I had absolutely no idea what was going on, despite being the core of NPM. Let's see it in detail.

First of all, we need to create a node-based application, and this is as easy as running the following command.

npm init

We'll be prompted a few questions about our project, such as the project name, the link to your Git repository, ect. But for now, let's just skip and press Enter.

Wait, use.

npm init -y

Amazing, we skipped all the questions.

So, we now have a package.json filled with some informations.

{
  "name": "your_directory_name",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

This file can be considered as the ID Card of our application. It contains its name, the current version, a tiny description, some keywords to help other people finding it, and a bunch of other stuff.

(For a complete list of the properties, including the 'bugs' keyword we talked about, please refer to the official documentation).

You're also free to update it whenever you want, as long as you respect the JSON format and use the correct properties.

Next, we want to start our application, right ?

Scripts

Let's look at some examples. I'll assume you have worked at least once with either an Express-based application, or one of the popular JS front-end frameworks (such as React, Angular or Vue).

This scripts property give you the power to customize npm commands to use within your application.

Wait a second.. Is that the place were the npm start I use everyday is defined ? Exactly.

"scripts": {
    "start": "node index.js"
}

You can specify any valid shells commands here, and create as much entries as you want nor need. You can even combine them !

"scripts": {
    ...,
    "stuffA:" : "...",
    "stuffB": "...",
    "together" : "npm run stuffA & npm run stuffB"
}

(Beware, this will run your scrips in parallel. To run concurrently, either replace the "&" by "&&", or look at the Concurrently package).

Now, npm run together ! And yes, this is not a typo, you need to write the run keyword. In fact, even the npm start command is launched as npm run start behind the scenes. (This ability is shared with a couple other keywords, such as install, test..)

You still there ? Nice, let's keep digging and unleash the full power of NPM by adding some dependencies !

Manage dependencies

Nowadays, an npm package already exists for pretty much anything. It would be a shame not to use them, and to rebuild the wheel everytime.

This is probably the biggest role of the package.json, it'll keep track of all the dependencies within your project, including the versions.

npm install <package_name>

This command will download all the files needed and install them into a brand new node_modules folder.
This folder will become bigger and badder as you'll install more and more packages (and the packages themselves most likely depend on others packages, which will be installed too).

Please don't do the same mistake as I did, and prevent committing this folder to your repository !

//.gitignore file at your project root

node_modules;

Dependencies & DevDependencies

Meanwhile, in your package.json ..

"dependencies": {
    "express": "^4.17.1" <--- Package version
},
"devDependencies": {
    "eslint": "^7.13.0"
}

What is this ? Actually, it's quite simple. Whenever you'll install something through the npm install command, it'll list it there. Doing so, when you'll share your amazing project with the world, the others devs will only launch npm install and all the libraries required for your project to run, will install nicely.

Now what are devDependencies ? Everything that is not vital for your application and that should be removed from your production build will go there (such as your linter, for example). Be careful, you have to manage them yourself.

By default, the npm install command will put everything inside the regular dependencies. To put something in the devDependencies, you must pass an extra argument to the command :

npm install --save-dev <your_library>

OR

npm install -D <your_library>

OR even shorter

npm i -D <your_library>

Organizing your dependencies will lead to better production performance. You might not need your linter rules or your Typescript types definition to run your app, right ?

Even better, npm allow us to omit the devDependencies on installation !

npm install --only=prod

Side (but important) notes

Uninstall a library

Made a typo and forgot the --save-dev in your command ?
Cleaning up your app from useless modules ?
You have two options, either remove the package and reinstall it again with the proper command, or do it manually in your package.json.

npm uninstall <your_library>

This will remove the library from the package.json and from the node modules.

In case you want to remove it from the node modules but not from the package.json (let's say the installation failed for whatever reason).

npm uninstall --no-save <your_library>

If you can't be bothered playing with the shell commands, you can also update manually your package.json.

Let's get back our previous example

"dependencies": {
    "express": "^4.17.1"
},
"devDependencies": {
    "eslint": "^7.13.0"
}

To remove the eslint dependency, erase it, and simply re-run npm install.

Install a specific version

Sometimes you'll have to install a specific version of a package. It's easy :

npm install <your_library>@<version>

For example :

npm install eslint@1.0.0

The package-lock.json

Last but not least, the package-lock. It's actually here to solve a problem from the package.json we haven't talked about.

We saw earlier that when installing a new package, his version is set into the package.json. It uses the semver convention.

Basically, the first character before the actual version number will implies some slight changes whenever the npm install command is ran.

"express": "~4.17.1"

The ~ mean that NPM will go and look for the 4.17.1 version, but if a newer patch release is available, let's say 4.18.1, it'll use this one.

"express": "^4.17.1"

The ^ mean that NPM will go and look for the 4.17.1 version, but if a newer minor release is available, let's say 4.17.9, it'll use this one.

"express": "4.17.1"

If you omit a character, NPM will always use this exact version whatever happens.

If you always specify the exact version, the following problem I'll explain is already out of sight

Now let's say you have been working for a couple years on a project, and a new contributor clones and installs it.

Since a lot of time passed, some of our dependencies must have received some new releases. As we explained earlier, depending on your semver convention, NPM will look for potential newer versions...

And there we hit the wall, your project and the newly installed one are actually different because some dependencies do not match the version specified in the package.json.

Package-lock to the rescue. As his name implied, it'll lock the version number in stone and will guarantee that the same package version is installed on every subsequent installation.