If you think about the underlying architecture of writing software, the program is usually comprised of a collection of modules (a module being some code that is grouped together, usually by file). If you have one or more modules that are program agnostic, meaning they can be reused in other programs, you'd create a "package".

Program
App.js <- Module
Dashboard.js <- Module
About.js <- Module
Math <- Package
add.js <- Module
subtract.js <- Module
multiply.js <- Module
divide.js <- Module

This package architecture is what makes the JavaScript ecosystem so powerful. If there's a package you need, odds are it's already been created and is available to download for free. Want to use Lodash? You'd download the lodash package. Want to use MomentJS to better manage timestamps in your app? Download the moment package. What about React? Yup, there's a react package. Now the question becomes, how and from where do we download these packages?

CDNs and script tags

The traditional way is to create a <script> tag that links to a CDN where the package is hosted or if you download it locally, the path to that file.

<body>
...
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="libs/react.min.js"></script>
</body>

This approach works, but it doesn't scale very well. First, if the CDN servers crash, your app crashes with it. Second, the order of the <script> tags matters. If library B is dependent on Library A, but the <script> tag to load library B comes before the <script> to load library A, things will break. Finally, you have a versioning problem. If jQuery releases a new version, you either need to manually swap out the CDN (assuming there's an updated one), or you'll need to re-download the new version to have it locally. Now for just one package this probably isn't a big deal, but as your application grows and you start having to manually manage 20+ packages, it's going to be a pain.

So let's try to conjure up a solution; here's a list of what we need.

  • Make it easier to download packages
  • Make it easier to upload packages for others to consume
  • Make it easier to switch versions of our packages
  • Do it all for free

Luckily for us, there's a company which solved all of our problems.

npm, Inc.

npm, Inc. is a for-profit, venture-backed company founded in 2014 and was acquired by Github in March of 2020. They host and maintain "npm" (short for Node.js package manager). npm consists of two parts: a registry (for hosting the packages) and a CLI (for accessing and uploading packages). At the time of this writing, the npm registry has over 800,000 packages being installed over 2 billion times a day by over 11 million JavaScript developers, 🤯.

Installing npm

In order to use the npm CLI, you'll need to install it. However, if you already have Node installed, you should already have npm as it comes with Node. If you don't have Node installed, you can download it from here or use a tool like Homebrew.

If Node and npm are installed correctly, you should be able to run the following commands in your terminal to check which versions you have installed.

node -v # My installed version: v11.10.0
npm -v # My installed version: 6.9.0

npm init

Now that you have Node and npm installed, the next step is to actually download a package. Before you do that, though, you'll want to initialize your new project with npm. You can do that by running npm init inside of your project's directory. This will walk you through some steps for initializing your project. Once finished, you'll notice you have a brand new package.json file and an empty node_modules directory.

node_modules

Whenever you install a package, the source code for that package will be put inside of the node_modules directory. Then, whenever you import a module into your project that isn't a file path, i.e. import React from 'react', your app will look to node_modules for the source.

package.json

You can think of your package.json file as containing all of the meta information for your project. It contains information like the project's name, author, description, and most important, the list of packages (as well as what versions) that your project depends on as well as how to run your project - here's an example.

{
"name": "github-battle",
"version": "1.0.0",
"description": "Compare two Github user's profile.",
"author": "Tyler McGinnis",
"license": "ISC",
"homepage": "https://github.com/tylermcginnis/react-course#readme",
"keywords": ["react", "react-router", "babel", "webpack"],
"repository": {
"type": "git",
"url": "git+https://github.com/tylermcginnis/react-course.git"
},
"main": "index.js",
"dependencies": {
"prop-types": "^15.7.2",
"query-string": "^6.2.0",
"react": "^16.8.3",
"react-dom": "^16.8.3",
"react-icons": "^3.4.0",
"react-router-dom": "^4.3.1"
},
"devDependencies": {
"@babel/core": "^7.3.4",
"@babel/plugin-proposal-class-properties": "^7.3.4",
"@babel/preset-env": "^7.3.4",
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.5",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"copy-webpack-plugin": "^5.0.0",
"css-loader": "^2.1.0",
"html-webpack-plugin": "^3.2.0",
"style-loader": "^0.23.1",
"webpack": "^4.29.5",
"webpack-cli": "^3.2.3",
"webpack-dev-server": "^3.2.1"
},
"scripts": {
"start": "webpack-dev-server --open",
"build": "NODE_ENV='production' webpack"
}
}

A few properties to point out.

dependencies

These are the packages your application needs to run. Whenever you install a new package, the source for that package will be placed into the node_modules directory and the name and version of that package will be added to the dependencies property in your package.json file.

devDependencies

If dependencies are the packages your application needs to run, devDependencies are the packages your application needs during development.

npm install

The reason it's so important to keep track of your dependencies and devDependencies is if someone downloads your project and runs npm install, npm will download all of the packages inside both dependencies and devDependencies and place them into the node_modules directory. This makes it so when you push your code to Github, instead of having to push up your entire node_modules directory, you can instead keep track of your dependencies and install them when needed using npm install.

The reason dependencies are separate from devDependencies is so you can build your app for production. In production, you don't care about the packages needed to develop your app; you only care about the packages needed to run your app.

scripts

You can use the scripts property to automate tasks. In the example above, we have two, start and build.

In order to run your script, cd into the same directory as the package.json file and from the command line run, npm run [NAME OF SCRIPT]. In our example, we have our start script running webpack-dev-server --open. In order to execute that script, from the command line we'd run npm run start.

Installing Packages

Now that we know all about initializing our project with npm init, node_modules, and the package.json file, the next step is to learn how to actually install a package from the npm registry. To do this, from the command line, run npm install package-name.

npm install react

That command will do a few things. It'll put the react package inside of our node_modules directory as well as add react as a property on our dependencies object inside our package.json file.

To tell npm you're installing a developer dependency (and it should be put it in devDependencies instead of dependencies), you'll append the --save-dev flag.

npm install webpack --save-dev

Publishing Packages

There wouldn't be over 800,000 packages on the npm registry if it wasn't easy to publish one. All you need to publish a package is an account on npm, a package.json file with name, version, and main (which points to the entry point of your package) properties.

Once you have those things, in your command line run npm login to login then npm publish to publish your package.

There are more advanced features about publishing that we won't go into in this post, but if you're curious, you can check out their official guide.

Versioning

Earlier one of our needs was the ability to more efficiently manage the different versions of the packages we were using. The npm CLI helps us out here as well.

Typically each package on the npm registry follows semantic versioning. There are three parts to semantic versioning, major versions, minor versions, and patch versions.

v1.2.3

In the version above, the major version is 1, the minor version is 2, and the patch version is 3.

The idea is if you're a library author and you had a breaking change, you'd increment the major version. If you had a new, non-breaking feature, you'd increment the minor version. For everything else, you'd increment the patch version.

So why is this important? We want to avoid having our app break because we installed the wrong version of a package. npm gives us some tools to prevent this.

^

If you look at the dependencies inside of our package.json file again, you'll notice that before each version number, there's a little ^.

"dependencies": {
"prop-types": "^15.7.2",
"query-string": "^6.2.0",
"react": "^16.8.3",
"react-dom": "^16.8.3",
"react-icons": "^3.4.0",
"react-router-dom": "^4.3.1"
}

What the ^ does is it instructs npm to install the newest version of the package with the same major version. So for example, if the prop-types package released v15.8.0, when we ran npm install on our project, we'd get that new version. However, if there was a breaking change and prop-types released v16.0.0, only the newest v15.X.X version would be installed and not the breaking v16.0.0 version.

~

If instead, you wanted to have both the major and minor version match, you'd use ~.

"dependencies": {
"prop-types": "~15.7.2"
}

Now, if v16.0.0 or v15.8.0 came out, neither would be installed. However, if v15.7.3 came out, it would be installed since its the newest version where both the major and minor versions match.

Exact version

Finally, if you wanted to only download the exact version of what's listed in your package.json file, you'd list only the version number.

"dependencies": {
"prop-types": "15.7.2"
}

Now, only v15.7.2 will ever be installed.


If you want to see a few less common options for specifying acceptable ranges, you can check out the Semver Calculator