DEV Community

Forrester Terry
Forrester Terry

Posted on

📦 How to Create, Publish, and Maintain a Node.js Package (in JavaScript and TypeScript)

Have you ever written a piece of code that could benefit other developers — or even your future self—over and over again? Perhaps it’s a utility function, a library, or a set of reusable tools you find yourself copying and pasting across multiple projects.

This might be the perfect candidate for an NPM package!

In this article, we’ll walk step-by-step through creating a simple Node.js package in both JavaScript and TypeScript, setting up best practices such as testing and versioning, and then publishing it to npmjs.com. 🚀


Table of Contents

1. Setting Up Your Project

2. Writing Your Package Code

3. Testing Your Package

4. Versioning Best Practices

5. Publishing Your Package

6. Common Features and Best Practices

7. Use Cases

8. Creating Scripts to Automate Common Tasks


1. Setting Up Your Project

1.1 Prerequisites

  • Node.js (v22.12.0 LTS is recommended as of January 2025, v18 LTS is the oldest version that is still maintained).
  • npm (comes bundled with Node).
  • A npmjs.com account for publishing your package.

1.2 Initialize Your Project

Create a new directory for your package:

mkdir my-awesome-package
cd my-awesome-package
Enter fullscreen mode Exit fullscreen mode

Initialize a new Node.js project:

npm init
Enter fullscreen mode Exit fullscreen mode

This command will ask several questions to help create a package.json file, the heart of your Node.js package. Key fields to pay attention to:

  • name: The name of your package (must be unique in the npm registry).
  • version: The version of your package, often starting at 1.0.0.
  • description: A brief explanation of your package.
  • entry point: The main file (often index.js or dist/index.js).
  • author: Your name or organization.
  • license: The license type (e.g., MIT).

You can skip the Q&A by running npm init -y to get a default package.json file.


2. Writing Your Package Code

We’ll explore two approaches: plain JavaScript and TypeScript. Choose whichever suits your project needs—or maintain both if you like.

2.1 Plain JavaScript Version

Create a file named index.js in your project root:

// index.js

/**
 * Returns a greeting message
 * @param {string} name - The name to greet
 * @return {string} A greeting message
 */
function greet(name) {
  return `Hello, ${name}!`;
}

// Export the function as a module
module.exports = greet;
Enter fullscreen mode Exit fullscreen mode

2.2 TypeScript Version

2.2.1 Install and Initialize TypeScript

Install Typescript as a development dependency:

npm install --save-dev typescript
Enter fullscreen mode Exit fullscreen mode

Initialize a tsconfig.json:

npx tsc --init
Enter fullscreen mode Exit fullscreen mode

2.2.2 A Recommended tsconfig.json

Replace the generated tsconfig.json with a configuration tailored for your needs, here we configure for modern Node.js (v22.12.0 LTS):

{
  "compilerOptions": {
    "target": "ES2020",                        // Modern JavaScript features
    "module": "CommonJS",                      // Node.js module system
    "rootDir": "src",                          // Source files directory
    "outDir": "dist",                          // Compiled output directory
    "strict": true,                            // Enable all strict type-checking options
    "esModuleInterop": true,                   // Better compatibility with CommonJS modules
    "skipLibCheck": true,                      // Skip type checking of declaration files for faster builds
    "forceConsistentCasingInFileNames": true,  // Enforce consistent file name casing
    "declaration": true,                       // Generate .d.ts files for type definitions
    "declarationMap": true                     // Generate source maps for declarations
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "**/*.test.ts"]
}
Enter fullscreen mode Exit fullscreen mode

This ensures compatibility with modern JavaScript and enforces strict type-checking, improving code quality.

2.2.3 Create Your Source File

Create a src directory and add index.ts:

// src/index.ts

/**
 * Returns a greeting message
 * @param name - The name to greet
 * @returns A greeting message
 */
export function greet(name: string): string {
  return `Hello, ${name}!`;
}
Enter fullscreen mode Exit fullscreen mode

2.2.4 Update package.json

We need to point main at the compiled JavaScript file in dist, and also specify where the type definitions live via the "types" field. We’ll also add a build script:

{
  "name": "my-awesome-package",
  "version": "1.0.0",
  "description": "A package that greets the user.",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "test": "jest"
  },
  "author": "Your Name",
  "license": "MIT"
}
Enter fullscreen mode Exit fullscreen mode

By specifying "types": "dist/index.d.ts", consumers of your package automatically get the TypeScript definitions.

Now, you can compile your TypeScript source by running:

npm run build
Enter fullscreen mode Exit fullscreen mode

3. Testing Your Package

Testing is not mandatory for publishing but is highly recommended to ensure reliability. We’ll demonstrate using Jest for both JavaScript and TypeScript.

3.1 Install Jest

npm install --save-dev jest
Enter fullscreen mode Exit fullscreen mode

For TypeScript, also install ts-jest and type definitions:

npm install --save-dev ts-jest @types/jest
Enter fullscreen mode Exit fullscreen mode

3.2 Configure Jest (TypeScript Only)

Initialize Jest with TypeScript support:

npx ts-jest config:init
Enter fullscreen mode Exit fullscreen mode

This creates a jest.config.js (or jest.config.ts) file tailored for TypeScript. Typically, it might look like:

// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node'
};
Enter fullscreen mode Exit fullscreen mode

3.3 Write Your Tests

Create a test directory. Let’s name our test file greet.test.js or greet.test.ts, depending on your language choice.

3.3.1 JavaScript Test Example

// test/greet.test.js
const greet = require('../index'); // Using JavaScript's require

test('returns a greeting with the given name', () => {
  expect(greet('Alice')).toBe('Hello, Alice!');
});
Enter fullscreen mode Exit fullscreen mode

3.3.2 TypeScript Test Example

// test/greet.test.ts
import { greet } from '../src/index'; // or '../dist/index' after build

test('returns a greeting with the given name', () => {
  expect(greet('Alice')).toBe('Hello, Alice!');
});
Enter fullscreen mode Exit fullscreen mode

3.4 Update package.json for Testing

Ensure your package.json includes:

{
  "scripts": {
    "build": "tsc",
    "test": "jest"
  }
}
Enter fullscreen mode Exit fullscreen mode

Now you can build and test your project via:

npm run build
npm test
Enter fullscreen mode Exit fullscreen mode

4. Versioning Best Practices

Node packages typically follow SemVer (Semantic Versioning):

  • MAJOR for incompatible API changes.
  • MINOR for adding functionality in a backward-compatible manner.
  • PATCH for backward-compatible bug fixes.

Example: 1.0.0 (Major.Minor.Patch)

To increment your package version (and create a Git tag if you’re using Git):

npm version patch   # e.g., 1.0.0 -> 1.0.1
npm version minor   # e.g., 1.0.1 -> 1.1.0
npm version major   # e.g., 1.1.0 -> 2.0.0
Enter fullscreen mode Exit fullscreen mode

5. Publishing Your Package

5.1 Log in to npm

Before you can publish, authenticate with npm:

npm login
Enter fullscreen mode Exit fullscreen mode

Enter your credentials and/or token. If you haven’t created an account yet, sign up here.

Alternatively, you can setup your .npmrc to handle the authentications:

echo //registry.npmjs.org/:_authToken=MY_TOKEN > ~/.npmrc
Enter fullscreen mode Exit fullscreen mode

You can change the registry path as needed, depending on your NPM repository instance. For instance, for GitHub you may do:

echo //npm.pkg.github.com/:_authToken=MY_TOKEN > ~/.npmrc
Enter fullscreen mode Exit fullscreen mode

5.2 Publish

For a plain JavaScript package:

npm publish
Enter fullscreen mode Exit fullscreen mode

For a TypeScript package, build your code before publishing:

npm run build
npm publish
Enter fullscreen mode Exit fullscreen mode

Your package is now live on the npm registry, and anyone can install it with:

npm install my-awesome-package
Enter fullscreen mode Exit fullscreen mode

If your package name is scoped (e.g., @your-scope/my-awesome-package), you might need:

npm publish --access public
Enter fullscreen mode Exit fullscreen mode

(--access public is required for scoped packages to be publicly available.)


6. Common Features and Best Practices

6.1 README.md

A clear README.md is essential for explaining your package. Include:

  • Installation Instructions
  npm install my-awesome-package
Enter fullscreen mode Exit fullscreen mode
  • Basic Usage
  // JavaScript:
  const greet = require('my-awesome-package');
  console.log(greet('Alice')); // Hello, Alice!
Enter fullscreen mode Exit fullscreen mode
  // TypeScript:
  import { greet } from 'my-awesome-package';
  console.log(greet('Alice')); // Hello, Alice!
Enter fullscreen mode Exit fullscreen mode
  • Advanced Examples: Show more use cases, configuration, or integration details.
  • API Documentation: If your package has multiple methods or classes, document them thoroughly.

6.2 LICENSE

Including a license clarifies how your package can be used. Define it in:

  1. A LICENSE file at the root.
  2. The license field in package.json.

6.3 GitHub Repository

While not strictly required, hosting your package on a platform like GitHub helps with:

  • Collaboration (issues, pull requests)
  • Version Control (Git tags, branches)
  • Automated CI (running tests on each commit)

6.4 Linting

Use a linter like ESLint for consistent coding standards.

npm install --save-dev eslint
npx eslint --init
Enter fullscreen mode Exit fullscreen mode

Configure your lint rules, then run:

npm run lint
Enter fullscreen mode Exit fullscreen mode

6.5 Continuous Integration (CI)

For open-source or team projects, consider GitHub Actions, Travis CI, or similar. This ensures tests run automatically on every commit, increasing confidence and catching issues early.

6.6 Adding Dependencies and Managing Dependency Files

As your package grows, you might need to incorporate other libraries or tools to enhance its functionality. Managing dependencies effectively ensures that your package remains robust, maintainable, and easy for others to use.

6.6.1. Adding Dependencies

Dependencies are packages required for your package to run correctly. DevDependencies are packages needed only during development, such as testing frameworks or build tools.

  • To add a runtime dependency:
  npm install lodash
Enter fullscreen mode Exit fullscreen mode

This command adds the lodash library as a dependency and updates your package.json:

  {
    "dependencies": {
      "lodash": "^4.17.21"
    }
  }
Enter fullscreen mode Exit fullscreen mode
  • To add a development dependency:
  npm install --save-dev eslint
Enter fullscreen mode Exit fullscreen mode

This adds eslint as a devDependency:

  {
    "devDependencies": {
      "eslint": "^8.0.0"
    }
  }
Enter fullscreen mode Exit fullscreen mode

6.6.2. Managing Dependency Files

  • package.json: This file lists all your dependencies and devDependencies. Ensure that all necessary packages are correctly listed to avoid missing dependencies during installation.

  • package-lock.json: Automatically generated when you install dependencies, this file locks the versions of your dependencies to ensure consistent installations across different environments. Do not manually edit this file; instead, let npm manage it.

6.6.3. Best Practices

  • Keep Dependencies Updated: Regularly update your dependencies to incorporate the latest features and security patches. You can use tools like npm-check-updates to assist with this.
  npm install -g npm-check-updates
  ncu -u
  npm install
Enter fullscreen mode Exit fullscreen mode
  • Minimize Dependencies: Only include packages that are essential for your package’s functionality. Fewer dependencies reduce the risk of conflicts and vulnerabilities.

  • Use Peer Dependencies When Appropriate: If your package is a plugin or extension that relies on a host package (like React), specify it as a peer dependency to avoid version conflicts.

  npm install --save-peer react
Enter fullscreen mode Exit fullscreen mode

Update your package.json:

  {
    "peerDependencies": {
      "react": "^17.0.0"
    }
  }
Enter fullscreen mode Exit fullscreen mode

6.6.4. Example: Adding a Dependency

Suppose you want to use axios for making HTTP requests in your package.

  1. Install axios as a dependency:
   npm install axios
Enter fullscreen mode Exit fullscreen mode
  1. Use axios in your code:
   // src/index.ts
   import axios from 'axios';

   /**
    * Fetches user data from an API
    * @param userId - The ID of the user
    * @returns User data
    */
   export async function fetchUserData(userId: number): Promise<any> {
     const response = await axios.get(`https://api.example.com/users/${userId}`);
     return response.data;
   }
Enter fullscreen mode Exit fullscreen mode

7. Use Cases

  1. Utility Packages
    • E.g., date-formatting libraries to simplify working with dates.
  2. UI Components
    • E.g., a set of React or Vue components published as a standalone package.
  3. CLI Tools
    • E.g., command-line utilities for scaffolding projects or automating tasks.
  4. Libraries for APIs
    • E.g., wrappers for third-party APIs or microservices, making them easy to integrate with Node.js.

8. Creating Scripts to Automate Common Tasks

To streamline your development workflow, it's beneficial to define custom scripts in your package.json. These scripts can handle tasks like building, testing, deploying, and versioning your package with simple commands.

8.1. Benefits of Custom Scripts

  • Automation: Reduce manual steps by automating build and deployment processes.
  • Consistency: Ensure that tasks are performed the same way every time.
  • Modularity: Create reusable scripts that can be combined for complex workflows.
  • Efficiency: Save time by chaining multiple commands into a single script.

8.2. Example: Deploying to Development and Production

Let's create two deployment scripts: deploy:dev for development environments and deploy:prod for production. These scripts will handle tasks such as building the project, tagging the Git repository, versioning, and deploying the package.

8.2.1. Defining the Scripts in package.json

Open your package.json and add the following scripts under the "scripts" section:

{
  "scripts": {
    "build": "tsc",
    "test": "jest",
    "version": "npm version patch",
    "tag:dev": "git tag -a v$(node -p \"require('./package.json').version\") -m \"Dev deployment v$(node -p \"require('./package.json').version\")\"",
    "deploy:dev": "npm run build && npm run test && npm run version && npm run tag:dev && git push && git push --tags && npm publish --tag dev",
    "tag:prod": "git tag -a v$(node -p \"require('./package.json').version\") -m \"Production deployment v$(node -p \"require('./package.json').version\")\"",
    "deploy:prod": "npm run build && npm run test && npm run version && npm run tag:prod && git push && git push --tags && npm publish"
  }
}
Enter fullscreen mode Exit fullscreen mode

8.2.2. Breakdown of the Scripts

  • build: Compiles the TypeScript code into JavaScript using the TypeScript compiler.
  • test: Runs the Jest test suite to ensure your package is working correctly.
  • version: Increments the patch version in package.json following Semantic Versioning.
  • tag:dev / tag:prod: Creates a Git tag corresponding to the current version for development or production deployments.
  • deploy:dev:

    1. Builds the project.
    2. Tests the build.
    3. Versions the package.
    4. Tags the commit for a development deployment.
    5. Pushes the commits and tags to the remote repository.
    6. Publishes the package to npm with the dev tag.
  • deploy:prod: Similar to deploy:dev but publishes the package without a tag, making it the latest stable release.

8.3. Running the Deployment Scripts

With the scripts defined, deploying becomes straightforward:

  • Deploy to Development:
  npm run deploy:dev
Enter fullscreen mode Exit fullscreen mode

This command will build your project, run tests, increment the patch version, create a Git tag, push changes, and publish the package with the dev tag.

  • Deploy to Production:
  npm run deploy:prod
Enter fullscreen mode Exit fullscreen mode

This command performs the same steps as deploy:dev but publishes the package as the latest stable version without an additional tag.

8.4. Making Scripts Modular

By breaking down tasks into smaller, reusable scripts (like build, test, version, and tagging), you can compose complex workflows easily. This modularity allows you to maintain and update individual steps without affecting the entire deployment process.

For example, if you need to change how the project is built, you only need to update the build script without modifying the deployment scripts.

8.5. Additional Automation Tips

  • Pre/Post Hooks: Utilize npm's pre and post script hooks to run tasks automatically before or after specific scripts. For instance, you can add a prepublishOnly script to ensure tests pass before publishing.
  {
    "scripts": {
      "prepublishOnly": "npm run test && npm run build"
    }
  }
Enter fullscreen mode Exit fullscreen mode
  • Environment Variables: Manage different environments (development, staging, production) by using environment variables within your scripts or leveraging packages like cross-env for cross-platform compatibility.

  • Error Handling: Incorporate error handling in your scripts to halt the process if a critical step fails, ensuring that incomplete or faulty deployments do not occur.

In Review

Publishing an NPM package may feel daunting, but the fundamentals are straightforward:

  1. Initialize your project with npm init.
  2. Write your code in either JavaScript or TypeScript (v5.7.2 recommended).
  3. Add tests to ensure your package remains reliable.
  4. Follow SemVer for version management.
  5. Publish your package with npm publish.

By following these best practices you’ll have a package that is reliable, easy to maintain, and simple for others to consume. 💻

Top comments (0)