When working with Node.js and its ecosystem, we often rely on third-party libraries to handle complex logic, which significantly speeds up the development process. These library are distributed as npm packages, which we are likely already familiar with. But have you ever wondered how these packages are created and published, allowing you to simply run a command like npm install
to use it in your projects? In this article, we will create a simple JavaScript library called magic-random
and walk through the step-by-step process of building and publishing it as an npm package.
1. Configuring package.json
As we know, npm is the package manager for Node.js and the package.json
file is the heart of every Node.js-based application. It is used in a variety of projects, from client-site applications such React, Vue or Angular to sever-side applications like Express or Next.js,. This file acts as a manifest file that provides metadata, dependencies, script and configurations for the project. This is why any JavaScript library shipped as an npm package must include this file in its codebase.
Let’s explore the settings included in a package.json
file:
Name and Version
The name
and version
are crucial s when we want to publish our package. As their names suggest, the name
field specifies the name of the package, and the version
field indicates the current version of the package. It’s important to note that the name
must be unique and not already exist in the npm registry. Additionally, the version
must be updated each time the package is published to reflect changes or updates.
{
"name": "magic-random",
"version": "1.0.0"
}
Description
The description
field provides a brief summary of what the package does. This field helps users quickly understand the purpose of the package. For example:
{
"description": "An JavaScript util to create a random value from a range or an array"
}
Keywords
The keywords
field accepts an array of string that represent a list of keywords. These keywords improve the discoverability of our package when users search it on npm. For instance:
{
"keywords": [
"random",
"magic",
"javascript"
]
}
Author
The author
field provides information about the creator of the package. The information includes the name (required), as well as the email and url (both optional).
The author
field can accepts an object, like this:
{
"author": {
"name": "Example",
"email": "owner@example.com",
"url": "https://example.com"
}
}
Alternatively, it can use a shorthand string format:
{
"author": "Example <address@example.com> (https://example.com)"
}
This field helps users identify and contact the author if needed.
Homepage
The homepage
field specifies the project’s homepage. This is typically the main page for the project, where users can find additional information or documentation. For example
{
"homepage": "https://example.com/magic-random"
}
Repository
The repository
field indicates where the project source code is hosted. It helps users and contributors locate the codebase:
{
"repository": {
"type": "git",
"url": "git+https://github.com/owner/magic-random.git"
}
}
Bugs
The bugs
field provide a way for users to report issues with the package. It accepts an object containing the url
and email
field:
{
"url": "https://github.com/owner/magic-random/issues",
"email": "owner@example.com"
}
License
The license
field specifies the license we use for our project. It helps people understand how they are permitted to use. If we do not include a license, our package is not considered an open source, and others can not use, copy, distribute or modify our project. Common licenses we can use includes: MIT, ISC, and others. We can use: https://choosealicense.com/licenses to select the license for our project and should not forget to include the LICENSE
file in the root directory of our project.
{
"license": "MIT"
}
Files
The files
field accepts an array representing a list of file pattern that describe entries to be included when our package is installed as a dependency.
{
"files": [
"dist"
]
}
Main
The main
field specifies the main entry file of our package. The path should be relative to the root directory of the project.
{
"main": "./dist/index.js"
}
Type
The type
field specifies the path of the bundled declaration file in case our package supports TypeScript:
{
"types": "./dist/index.d.mts",
}
Scripts and DevDependencies specified for the example library
{
"scripts": {
"clean": "rimraf dist",
"build": "npm run clean && npm run bundle",
"bundle": "tsup-node src/index.ts --dts --format esm,cjs",
"test": "jest"
},
"devDependencies": {
"@alloc/fast-rimraf": "^1.0.8",
"@types/node": "^22.10.2",
"prettier": "^3.4.2",
"tsup": "^8.3.5",
"@types/jest": "^29.5.0",
"jest": "^29.6.0",
"ts-jest": "^29.1.0",
},
}
@alloc/fast-rimraf
: A tool to delete a huge directory fast.
@types/node
: TypeScript definitions for node.
prettier
: An opinionated code formatter.
tsup
: Bundle TypeScript libraries with no config, powered by esbuild.
jest
: The testing framework used to run the tests.
@types/jest
: Type definitions for Jest to enable TypeScript support.
ts-jest
: A Jest preset that allows Jest to work seamlessly with TypeScript.
2. Set up the Project
/magic-random
├── node_modules/
├── dist/
├── src/
├── index.ts
├── .gitignore
├── .prettiercc
├── package.json
├── packge-lock.json
├── LICENSE
├── README.md
├── tsconfig.json
In practice, the project can be evolved and be contributed by other developers. Ensuring the codebase’s quality, readability and maintainability by applying unit tests and following a few conventions is necessary from the beginning. That is why, in this example, we set up unit test with Jest, format rules with Prettier and type rules with TypeScript.
Here’s an example of .prettiercc
file:
{
"bracketSpacing": true,
"jsxBracketSameLine": true,
"singleQuote": true,
"semi": true,
"trailingComma": "es5"
}
Example for tsconfig.json
file:
{
"compilerOptions": {
"declaration": true,
"module": "es2020",
"target": "es2020",
"lib": ["es2020"],
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true
},
"include": [
"src"
],
"exclude": [
"node_modules",
"dist"
]
}
Example for jest.config.ts
file:
import type {Config} from 'jest';
const config: Config = {
preset: "ts-jest",
clearMocks: true,
collectCoverage: true,
coverageDirectory: "coverage",
coveragePathIgnorePatterns: [
"/node_modules/"
],
coverageProvider: "v8",
testEnvironment: "node",
testPathIgnorePatterns: [
"/node_modules/"
],
};
export default config;
Don’t forget to ignore unnecessary folders by adding them to .gitignore
files
/node_modules
/dist
3. Develop the Library
In the index.ts
file, we define a function magicRandom
as follows:
function magicRandom(min: number, max: number): number{
if(isNaN(min) || isNaN(max)) {
throw new Error('Parameters must be numbers')
};
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export default magicRandom;
And add a unit test to index.spec.ts
file:
import magicRandom from './index';
describe('magicRandom', () => {
it('should return a number within the given range (inclusive)', () => {
const min = 1;
const max = 10;
const result = magicRandom(min, max);
expect(result).toBeGreaterThanOrEqual(min);
expect(result).toBeLessThanOrEqual(max);
});
it('should throw an error if parameters are not numbers', () => {
expect(() => magicRandom(NaN, 10)).toThrow('Parameters must be numbers');
expect(() => magicRandom(1, NaN)).toThrow('Parameters must be numbers');
expect(() => magicRandom(NaN, NaN)).toThrow('Parameters must be numbers');
});
it('should work correctly when min equals max', () => {
const min = 5;
const max = 5;
const result = magicRandom(min, max);
expect(result).toBe(5);
});
});
Then, we run the unit tests before bundling the library via this command:
npm run tetst
This is the main function of our library. The next step is to build it locally.
We run the following command in the terminal:
npm run bundle
4. Testing Unpublished Package
There are two ways to test our package before it has been published:
a. Using npm link
In the root directory of our project, we open the terminal and run the following command:
npm link
This command will create a symlink in the global folder of npm. We can verify if our package is linked by running this command:
$ npm ls --global
/home/owner/.nvm/versions/node/v18.18.0/lib
├── corepack@0.19.0
├── npm@9.8.1
└── magic-random@1.0.0 -> ./../../../../../magic-random
As we can see, our package magic-random
is successfully linked to the project’s directory
In the root directory of the consumer project (project that will install our package), we run the following command:
npm link magic-random
This command will create a symbolic link from globally-installed magic-random
to node_modules/
of the current directory. Then, we can start using the library as if we had published and installed it from npm.
import magicRandom from 'magic-random';
b. Install package directory
This approach is useful when our package project and consumer project use different package managers, like NPM and PMPM. The package will be symlinked into the node_modules
folder of consumer project by running:
npm install path/to/local/package
5. Create Readme
Readme is an important file in open-source projects. It helps developers understand why we created the projects and how to use it properly. In a README.md
file we should include the followng information:
Package introduction (including live demo, if possible)
Key features
Installation
Usage
API references
The package’s dependencies (optional)
Contributing (if this package is open to contributed)
Limitations (optional)
The License
6. Publish the Package
After verifying that everything is in order, it’s time to publish our package to NPM:
Ensuring all tests have passed and the code is properly formatted.
Don’t forget to commit and push our code changes to correct remote repository that we specified in
package.json
file.Next, access NPM page at: https://www.npmjs.com/ and login if you already have an account.If you don’t have one, sign up to create a new account.
In the project terminal, log into NPM via the command
npm login
Check if the package name is available by running the following command
npm seach magic-random
If the result is:
No matches found for "magic-random"
, it means we can use this name. Otherwise we need to find another name.Finally, we run the below command to publish our package:
npm publish
And the result should look like this:
npm notice npm notice 📦 magic-random@1.0.0 npm notice === Tarball Contents === npm notice 1.0kB package.json npm notice 1.2kB README.md npm notice 0 LICENSE npm notice 2.1kB dist/index.js npm notice === Tarball Details === npm notice name: magic-random npm notice version: 1.0.0 npm notice package size: 2.3 kB npm notice unpacked size: 4.3 kB npm notice shasum: abc123... npm notice integrity: sha512-xyz... npm notice total files: 4 npm notice + magic-random@1.0.0
After successfully publishing our package by running command in the terminal, we can check it again on the NPM page via the URL like this: npmjs.com/package/package-name.