How to create and publish an NPM package.

How to create and publish an NPM package.

·

8 min read

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.