Creating a modern JS library: package.json and dependencies
Tags: npm, node, javascript
Published: 2021-04-05 15:20:03

Your `package.json` is among the most important files in your project. It handles dependencies, exports, versioning, naming, etc. `package.json` effectively includes all the metadata a user would need to use your library. Therefore, it's critical that you create `package.json` properly; if you don't, about half of your bug reports will be issues involving imports, mismatched dependencies, etc.

Let's go through the fields that a typical `package.json` will contain. We'll be creating an example package for re-encoding UTF-8 data or strings into the fictitious "Catlang" format.

### `name` (required)
The name of your library. Even if you have a preferred style, the convention is to use all-lowercase letters and dashes to separate words.

If you're creating a fork of an existing package, don't add a number to the end: either describe what you changed or, if that's too difficult, express the same idea with different words.

Poor choice of name:
```json
{
  "name": "EncodeInCatlang2",
}
```
Good choice of name:
```json
{
  "name": "encode-in-catlang"
}
```
If the above was already taken:
```json
{
  "name": "catlang-encoder"
}
```

### `version` (required)
The current version of the package. You will change this every time you want to publish a new version of your package. Try to follow semantic versioning (more details on what this is later). My suggestions are as follows:
- When you first create a package, use `0.0.1`.
- Whenever you fix a bug, you want a "patch" revision. Increment the last digit.
  - `1.0.1` → `1.0.2`
  - `3.4.9` → `3.4.10`
- Whenever you add a new feature, soft-deprecate (i.e. discourage) an existing feature, or do anything else that doesn't break code that previously worked fine, you want a "minor" revision. Increment the second-to-last digit and set the last digit to zero.
  - `1.0.1` → `1.1.0`
  - `3.9.0` → `3.10.0`
  - `0.3.5` → `0.3.6`<sup>*</sup>
- Whenever you hard-deprecate (i.e. remove) an existing feature, change the behavior of something, or otherwise do anything that will break code that worked fine on a prior version, you must use a "major" revision. Increment the first digit and set the other two to zero.
  - `1.1.3` → `2.0.0`
  - `10.1.3` → `11.0.0`
  - `0.3.5` → `0.4.0`<sup>*</sup>
  - `0.0.3` → `0.0.4`<sup>*</sup>

Note the examples with an asterisk. For versions below `1.0.0`, a patch revision is not possible and the other two types shift down; incrementing the second-to-last digit is major and the last digit is minor. For versions below `0.1.0`, this is even more severe, since every version bump is a new major version.

This is why updating from `0.0.X` to `0.1.0` and from `0.X.X` to `1.0.0` are what I like to call "maturity" revisions; they completely change the meaning of each digit. As good practice, try to reduce the number of major versions you make after `1.0.0`, but use as many minor and patch versions as you'd like.

As a notation guide, semantic versions are usually prefixed with a "v" outside of `package.json`. When referring to version `1.2.3` in a GitHub issue, for example, say "v1.2.3".

You also may want to note that alpha, beta, and release candidate versions are supported by semantic versioning. For example, `1.0.0-alpha.1`, `2.2.0-beta.4`, `0.3.0-rc.0`. Typically, the patch version is unset for these prerelease versions, and they aren't downloaded by package managers unless the user requests a prerelease version.

Last thing: NPM considers packages under v1.0.0 to be unstable and ranks them lower in the search. If you want a quick boost to search score, make your library stable!

Let's update our `package.json`:
```json
{
  "name": "catlang-encoder",
  "version": "0.0.1"
}
```

### `description` (strongly recommended)
A brief description of what your package does. Even if the name is self-explanatory, it doesn't hurt to repeat yourself. The description is used for search results in NPM, so make sure to highlight the library's most major features.

```json
{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter"
}
```

### `author` (strongly recommended)
The name (and optionally email and website) of the author of the package. Optimally, you will use your full name here, but if you're not comfortable doing so, your online alias will suffice. The field can take one of two formats:

```json
"Your Name <youremail@yourdomain.com> (https://yoursite.com)"
```
or
```json
{
  "name": "Your Name",
  "email": "youremail@yourdomain.com",
  "url": "https://yoursite.com"
}
```

The email and website are optional, but you must add your name or alias.

New `package.json`:
```json
{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>"
}
```

### `license` (strongly recommended)
The license under which your library's code may be used. We'll get into licensing more in another article, but for now you should at least know the syntax.

If you're using a common license (such as MIT, ICS, BSD, GPL, Apache, etc.), you can use its identifier, which you can find in [this list](https://spdx.org/licenses/). Try to pick a license from that list, but if you can't, mention the file containing your license instead:

```js
"SEE LICENSE IN LICENSE.md"
```

If you'd like to distribute your library under multiple licenses, you can use `OR` and `AND` expressions with parentheses. If you'd like to specify an exception within some license, use the `WITH` keyword.

```json
"(Apache-2.0 OR MIT)"
"(GPL-3.0-only WITH Bison-exception-2.2)"
```

If you don't know anything about licensing and just want to freely distribute your code, `"MIT"` is a safe option.

New `package.json`:
```json
{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "license": "MIT"
}
```

### `keywords` (recommended)
The keywords to associate with your library in the NPM search. These are a way of getting your package in searches that don't include any words in the `name` or `description` fields. The point of the `keywords` field is to prevent keyword spamming in the description or title, but you still shouldn't use unrelated terms within the keywords.

Adding keywords to `package.json`:
```json
{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "license": "MIT"
}
```

### `homepage` (recommended)
The website for your project. This can be a documentation site, example page, etc. If you have a webpage that includes information about your library, even a blog post, use it here. Avoid using the link to your source code (i.e. your GitHub repository) unless you have absolutely no other site to link to.

```json
{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "homepage": "https://catlangencoder.js.org",
  "license": "MIT"
}
```

### `repository` (recommended)
Information about the repository. Assuming you're hosting your source code on a version control system (if you're not, you definitely should), use an object with `type` and `url` keys:

```json
{
  "type": "git",
  "url": "https://domain.com/your-name/your-library.git"
}
```

There are some shorthands, such as using just the URL and letting NPM guess what type the repository is, but I advise against doing this for the sake of clarity.

If your library is a part of a monorepo, you can specify the `directory` subfield to denote the subdirectory within which the package is contained. If you aren't using a monorepo or don't know what a monorepo is, don't use `directory`.

```json
{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "author": "Cat <cat@gmail.com>",
  "description": "Fast Unicode to Catlang converter",
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "homepage": "https://catlangencoder.js.org",
  "repository": {
    "type": "git",
    "url": "https://github.com/cat/catlang",
    "directory": "js/packages/catlang-encoder"
  },
  "license": "MIT"
}
```

### `bugs` (recommended)
Where users should report issues with the library. GitHub has a built-in issue tracker, so often you'll be using the `/issues` subdomain of your GitHub repository for this. You can specify just a string if you'd like just this URL:

```json
"https://github.com/your-name/your-library/issues"
```

However, if you'd also like to add an email that users can report bugs to, you can use the object form:

```json
{
  "email": "youremail@yourdomain.com",
  "url": "https://github.com/your-name/your-library/issues"
}
```

Updated `package.json`:
```json
{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "homepage": "https://catlangencoder.js.org",
  "repository": {
    "type": "git",
    "url": "https://github.com/cat/catlang",
    "directory": "js/packages/catlang-encoder"
  },
  "bugs": {
    "email": "cat@gmail.com",
    "url": "https://github.com/cat/catlang/issues"
  },
  "license": "MIT"
}
```

### `engines` (optional)
The environments in which your library will work. This is only applicable for libraries that support Node.js (e.g. a CSS library shouldn't use this field). If your library does not use modern features of JavaScript (such as async iterators), you also don't need to specify this field. The format is as follows:

```json
{
  "node": ">=0.10.3 <15"
}
```

For now, `node` is the only key of the `engines` field that you'll need to use.

Suppose `catlang-encoder` needs support for ES6 features such as `Promise` + `async`/`await`, `for..of`, etc. Since `async`/`await` was only added in v7.6.0, we use that as the minimum version.

```json
{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "homepage": "https://catlangencoder.js.org",
  "repository": {
    "type": "git",
    "url": "https://github.com/cat/catlang",
    "directory": "js/packages/catlang-encoder"
  },
  "bugs": {
    "email": "cat@gmail.com",
    "url": "https://github.com/cat/catlang/issues"
  },
  "engines": {
    "node": ">=7.6.0"
  },
  "license": "MIT"
}
```

### `contributors` (optional)
People other than the author who have contributed in a major way to the project. The format is an array of objects or strings in the same format as the `author` field. For example:

```json
[
  "John Doe <me@johndoe.net> (johndoe.net)",
  {
    "name": "Place Holder",
    "email": "placeholder@gmail.com"
  }
]
```

If you worked on this project mostly alone (perhaps with a few pull requests here and there), you don't need to specify this field. However, if someone has helped you many times, it's a good idea to add them.

```json
{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "contributors": [
    "Cat 2"
  ],
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "homepage": "https://catlangencoder.js.org",
  "repository": {
    "type": "git",
    "url": "https://github.com/cat/catlang",
    "directory": "js/packages/catlang-encoder"
  },
  "bugs": {
    "email": "cat@gmail.com",
    "url": "https://github.com/cat/catlang/issues"
  },
  "engines": {
    "node": ">=7.6.0"
  },
  "license": "MIT"
}
```

### `bin` (optional)
The location of the package's binary. If you're developing a CLI tool, set this to the entry point of your codebase. The file you set will be executed whenever someone runs `npm run your-package` or `yarn run your-package`. Of course, you don't need this field if you don't want to provide a CLI tool with your package.

For a single executable, the field can just be a string:
```json
"path/to/bin.js"
```

If you have more than one executable, you can specify a mapping as so:

```json
{
  "command-1": "./bin1.js",
  "command-2": "./bin2.js"
}
```

If we have a CLI executable for quick-and-dirty Catlang encoding from the command line at `lib/cli.js`:

```json
{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "bin": "lib/cli.js",
  "contributors": [
    "Cat 2"
  ],
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "homepage": "https://catlangencoder.js.org",
  "repository": {
    "type": "git",
    "url": "https://github.com/cat/catlang",
    "directory": "js/packages/catlang-encoder"
  },
  "bugs": {
    "email": "cat@gmail.com",
    "url": "https://github.com/cat/catlang/issues"
  },
  "engines": {
    "node": ">=7.6.0"
  },
  "license": "MIT"
}
```

### `private`
Prevents your package from being published if set to `true`. Obviously, you shouldn't have this field in your `package.json` but some starter projects/templates include `"private": true` in `package.json` to prevent you from accidentally publishing code that isn't meant to be a package. You'll want to remove the `private` field if it exists; otherwise, NPM will refuse to publish your package.

There are a few rarer fields that you may occasionally need such as `os` and `man`, in which case you should take a look at the [original documentation for `package.json`](https://docs.npmjs.com/cli/v7/configuring-npm/package-json). In addition, we'll be making use of the `scripts` field later on, and if you aren't familiar with it, you should [read this](https://docs.npmjs.com/cli/v7/using-npm/scripts).

## Dependencies and exports

You may have noticed that our `package.json` for `catlang-encoder` has no dependencies and has no exports. We'll get into how you should handle exports in the next article, since it's quite a complicated topic, but right now we'll discuss dependencies in `package.json`.

As a rule of thumb, you should try to minimize the number of dependencies you use. If a dependency has under 20 lines of source code, the logic is probably simple enough that you can rewrite it on your own because that will make it easier to maintain your codebase.

If you do end up needing dependencies, there are four fields you can use to specify them: `dependencies`, `peerDependencies`, `optionalDependencies`, and `devDependencies`.

### `dependencies`
The mapping of package name to versions supported for your library's runtime dependencies. If you use code from another library at runtime (i.e. when someone uses your package), add that library to this field. The syntax is as follows:

```json
{
  "some-package": "^2.3.1",
  "other-package": ">=7.0.0",
  "last-package": ">=2 <3"
}
```

The keys of the object are the names of the dependencies, while the values are the versions to accept. The syntax for specifying versions is called semantic versioning, or "semver". The full details are detailed [on the semantic versioning website](https://semver.org/), but generally you need to know only two things:
- The actual version of a package is always three numbers separated by periods, as in the `version` field of `package.json`
- Dependencies in `package.json` can use version identifiers, which refer to one or more versions of the package

When your users install your package, their package manager will see all the dependencies in `package.json` and download the relevant ones
There are many types of version identifiers, but the most relevant ones are these:

- Exact identifiers, which are just the version numbers. They may exclude the patch and minor versions.
  - `1.0.1` matches only v1.0.1
  - `1.0.0-rc.0` matches only v1.0.0-rc.0 (this is the only way to get a prerelease version of a package)
  - `0.10` matches any version in the v0.10 range (at least v0.10.0, before v0.11.0)
  - `1` matches any version in the v1 range (at least v1.0.0, before v2.0.0)
- Comparative identifiers, which match versions above or below a specific version
  - `>1.1.3` matches versions more recent than v1.1.3 (v1.1.4, v2.0.4, etc. all work)
  - `<=2.8.7` matches versions older than or equal to v2.8.7 (v2.8.7, v1.0.1, v0.0.1 all work)
- Match-all identifiers, which use `x` or `*` to mark a part of the semver string that can be any version
  - `1.x` matches any version in the v1 range (like `1` does)
  - `*` matches all versions of the package
  - `2.3.*` matches any version in the v2.3 range (like `2.3`)
  - **Careful: `2.*.0` matches any version in the v2 range, not just patch-0 versions**
- Second-digit identifiers, which use tildes to match the second digit of the version provided that the third digit is greater than or equal to that specified in the identifier
  - `~1.2.3` matches all versions `>=1.2.3` and `<1.3.0`
  - `~0.4.0` matches all versions `>=0.4.0` and `<0.5.0`
- Major version matchers, which use `^` to match the first nonzero digit
  - Technically, the first digit, zero or nonzero, is always the major version. However, when the first digit is zero, a bump to the second digit is a significant change, and `^` prevents your library from accidentally accepting that significant, possibly breaking change.
  - **This is the most popular matcher**
  - `^3.2.1` matches any version in the v3 range
  - `^0.4.0` matches any version in the v0.4 range
  - `^0.0.5` matches just v0.0.5

Last thing: you can combine version identifiers using a space between two of them. The new identifier matches if both of the subidentifiers match. In other words:
- `>=1.5 <3` matches versions that are at least v1.5 but below v3 (i.e. at most v2)
- `1.x <=1.8` matches versions that start with v1 but are at most v1.8

If you're not sure that your semver string is correct, you can always try [this tool](https://semver.npmjs.com/) to test that it matches the versions of your dependency in the way you want  it to.

Let's say we need `catlang-dictionary` to tell us which words have direct translations to shorter glyphs in Catlang, and we have found that version 1.2.3 works well. Assuming that `catlang-dictionary` follows semantic versioning, it's a good idea to use `^1.2.3` as the version identifier.

```json
{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "bin": "lib/cli.js",
  "contributors": [
    "Cat 2"
  ],
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "homepage": "https://catlangencoder.js.org",
  "repository": {
    "type": "git",
    "url": "https://github.com/cat/catlang",
    "directory": "js/packages/catlang-encoder"
  },
  "bugs": {
    "email": "cat@gmail.com",
    "url": "https://github.com/cat/catlang/issues"
  },
  "engines": {
    "node": ">=7.6.0"
  },
  "dependencies": {
    "catlang-dictionary": "^1.2.3"
  },
  "license": "MIT"
}
```

### `peerDependencies`
The dependencies for which your package was installed as an add-on, extension, or integration. The difference between `dependencies` and `peerDependencies` is that `peerDependencies` are not automatically installed and are typically used to denote what your library integrates with or extends.

It's hard to define exactly when you should use a peer dependency over a dependency, but if the user installed your library only because they were directly using a certain package, add that package to the `peerDependencies`.

For instance, a React component library would have `"react"` in `peerDependencies`, and a Babel plugin would have `"@babel/core"` in `peerDependencies`. On the other hand, a JavaScript wrapper for a REST API would probably leave `node-fetch` in `dependencies` rather than `peerDependencies`. `node-fetch` is not being used directly by the user and is not the reason the package was installed; it's simply important for the HTTP requests to go smoothly.

It's very important that you **use a loose version identifier for peer dependencies**. For example, if you use `~16.3` as the version of React in your React component library, when your user updates to React v16.8, they'll get warnings about incompatible versions even though your library probably still works in v16.8. You'd be better off using `^16.3`, or if you think the next major version won't break your package, `>=16.3`.

Since `catlang-encoder` works universally, regardless of framework, we don't need any peer dependencies and won't need to modify our `package.json`.

### `optionalDependencies`
Any dependencies you would like to have but can do without. Effectively, there's no guarantee that these dependencies will be installed: they are usually installed if the package is compatible with the operating system and if the user agrees to installing that dependency. The primary use case for this field is preventing your package from failing to install when one of your dependencies is incompatible with the processor architecture, operating system, etc.

It's important to note that **a dependency that can be installed for extra functionality is an optional peer dependency**. If you can improve or add functionality to a section of your code if a dependency is installed, that's an optional peer dependency and not an optional dependency because you don't want the dependency installed by default. 

For example, the `@discordjs/opus` extension for `discord.js` enables support for certain voice calling features in the Discord API. Since many users of the Discord API won't need voice support at all, using `@discordjs/opus` in `optionalDependencies` would install it by default, adding bloat. Therefore, it's an optional peer dependency, i.e. `@discordjs/opus` is in `peerDependencies` and it is specified as optional using the `peerDependenciesMeta` field:

```json
{
  "@discordjs/opus": {
    "optional": true
  }
}
```

(As a side note, the actual `discord.js` does not do this anymore; they've completely removed the dependency from `package.json` and just ask users in the README to install the optional dependencies if they want them.)

For `catlang-encoder`, we can optionally use the native `utf-8-validate` package to verify that the inputs to the encoder are valid, but it's not necessary because the validation isn't strictly necessary. Since generally, most users don't need it, we make it an optional peer dependency. (Remember to use a loose version matcher with peer dependencies! We'll use `*` to support any version of `utf-8-validate`.)

On the other hand, we want to use `catlang-concat` whenever possible to more efficiently concatenate Catlang buffers, but we *can* still do normal buffer concatenation without it, so we specify it as an optional dependency to effectively tell the package manager: "I really want catlang-concat if you can install it, but if not I'll still work without it."

```json
{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "bin": "lib/cli.js",
  "contributors": [
    "Cat 2"
  ],
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "homepage": "https://catlangencoder.js.org",
  "repository": {
    "type": "git",
    "url": "https://github.com/cat/catlang",
    "directory": "js/packages/catlang-encoder"
  },
  "bugs": {
    "email": "cat@gmail.com",
    "url": "https://github.com/cat/catlang/issues"
  },
  "engines": {
    "node": ">=7.6.0"
  },
  "dependencies": {
    "catlang-dictionary": "^1.2.3"
  },
  "peerDependencies": {
    "utf-8-validate": "*"
  },
  "peerDependenciesMeta": {
    "utf-8-validate": {
      "optional": true
    }
  },
  "optionalDependencies": {
    "catlang-concat": "^4.5.6"
  },
  "license": "MIT"
}
```

### `devDependencies`
The list of dependencies that are not needed at runtime but are needed to develop the library. These packages are never installed when a user downloads your package; however, when *you* run `npm`/`yarn`/`pnpm install`, those packages are added. This is most often filled with packages needed to build the source code into runtime code, if any is ncessary. For instance, you'll often see [`babel`](https://babeljs.io/) for projects using JSX, or the [`typescript`](https://npmjs.com/package/typescript) package for any library with source code in TypeScript.

Since we love stopping type errors before runtime, we have TypeScript source code. We'll need to add the `typescript` package to our `devDependencies` in order to use the `tsc` compiler, which will ultimately allow us to allow both TypeScript and JavaScript consumers to use our `catlang-encoder`.

```json
{
  "name": "catlang-encoder",
  "version": "0.0.1",
  "description": "Fast Unicode to Catlang converter",
  "author": "Cat <cat@gmail.com>",
  "bin": "lib/cli.js",
  "contributors": [
    "Cat 2"
  ],
  "keywords": [
    "catlang",
    "cat language",
    "catlang converter",
    "high performance"
  ],
  "homepage": "https://catlangencoder.js.org",
  "repository": {
    "type": "git",
    "url": "https://github.com/cat/catlang",
    "directory": "js/packages/catlang-encoder"
  },
  "bugs": {
    "email": "cat@gmail.com",
    "url": "https://github.com/cat/catlang/issues"
  },
  "engines": {
    "node": ">=7.6.0"
  },
  "dependencies": {
    "catlang-dictionary": "^1.2.3"
  },
  "peerDependencies": {
    "utf-8-validate": "*"
  },
  "peerDependenciesMeta": {
    "utf-8-validate": {
      "optional": true
    }
  },
  "optionalDependencies": {
    "catlang-concat": "^4.5.6"
  },
  "devDependencies": {
    "typescript": "^4.2.2"
  },
  "license": "MIT"
}
```

With that, we've finished going through the vast majority of the fields of `package.json`. In the next article, we'll discuss how to add proper exports to `package.json`, which is critical if you want to develop a package that supports users whether they're using a CDN, a package manager, or a buildless web app with ESM.