Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Building and Publishing an NPM Package

Creating your own NPM package allows you to share reusable code with the world, contribute to the open-source ecosystem, and establish your presence as a developer. But more importantly, understanding the package lifecycle — from initialization to publishing to maintenance — transforms you from someone who consumes packages into someone who understands the entire Node.js dependency system. When something goes wrong with a dependency (and it will), that understanding is invaluable. This comprehensive guide will walk you through every step of building a professional-grade NPM package, with a focus on the decisions that separate a throwaway experiment from a package people actually trust and use.

Why Create an NPM Package?

Benefits of Publishing Packages

  1. Code Reusability: Use your code across multiple projects without copy-pasting. When you fix a bug in the package, every project that depends on it gets the fix via a version bump.
  2. Community Contribution: Help other developers solve problems they should not have to solve from scratch. The best packages codify hard-won knowledge about edge cases and gotchas.
  3. Portfolio Building: A published package with tests, documentation, and real downloads says more about your skills than most portfolio websites. Interviewers can read your actual production code.
  4. Version Control: Maintain and update code systematically using semantic versioning, so consumers know exactly what to expect from each release.
  5. Collaboration: Allow others to contribute improvements, report bugs, and expand functionality in ways you never imagined. Some of the best features in popular packages came from community PRs.

When to Create a Package

Create a package when you:
  • Have code you use repeatedly across projects
  • Solve a problem that others might face
  • Want to open-source a tool or utility
  • Need to share internal libraries across teams
  • Want to learn about package development

Planning Your Package

Choosing a Package Name

Your package name must be:
  • Unique: Check availability on npmjs.com
  • Descriptive: Clearly indicate what the package does
  • Lowercase: NPM package names are case-insensitive
  • URL-safe: No spaces, only hyphens allowed
# Check if name is available
npm view package-name-here

# If not found, it's available!
# Error: npm ERR! 404 'package-name-here' is not in this registry.

Naming Strategies

// Good names (descriptive, clear -- a developer should guess what it does from the name)
'date-formatter'          // Formats dates -- clear and specific
'json-validator'          // Validates JSON -- no ambiguity
'log-colorizer'           // Adds color to logs -- searchable
'express-rate-limiter'    // Rate limiting for Express -- framework prefix helps discovery

// Scoped packages (for personal/organization use)
// The @ prefix creates a namespace, like a folder for your packages
'@username/utilities'     // Personal namespace -- avoids collision with "utilities"
'@company/api-client'     // Organization namespace -- common for internal tooling
'@myorg/design-system'    // Scoped packages can be public or private
Scoped packages (starting with @) allow you to namespace your packages and avoid naming conflicts. They’re free for public packages!

Defining Package Requirements

Before coding, define:
  • Purpose: What problem does it solve?
  • Target Audience: Who will use it?
  • Dependencies: What external packages are needed?
  • API Design: How will users interact with it?
  • Compatibility: Which Node versions will it support?

Project Structure

Basic Package Structure

my-awesome-package/
├── src/                    # Source code
│   ├── index.js           # Main entry point
│   ├── utils/             # Utility functions
│   └── validators/        # Validation logic
├── test/                   # Test files
│   ├── index.test.js
│   └── utils.test.js
├── examples/               # Usage examples
│   └── basic-usage.js
├── .gitignore             # Git ignore rules
├── .npmignore             # NPM ignore rules
├── LICENSE                # License file
├── README.md              # Documentation
├── CHANGELOG.md           # Version history
└── package.json           # Package manifest

Advanced Package Structure

my-library/
├── src/
│   ├── index.js           # Public API
│   ├── core/              # Core functionality
│   │   ├── parser.js
│   │   └── validator.js
│   ├── utils/             # Helper functions
│   │   ├── formatters.js
│   │   └── constants.js
│   └── types/             # TypeScript definitions
│       └── index.d.ts
├── dist/                   # Compiled/bundled output
│   ├── index.js           # CommonJS
│   ├── index.esm.js       # ES Module
│   └── index.d.ts         # Type definitions
├── test/
│   ├── unit/              # Unit tests
│   ├── integration/       # Integration tests
│   └── fixtures/          # Test data
├── docs/                   # Additional documentation
│   ├── api.md
│   └── examples.md
├── scripts/                # Build/utility scripts
│   ├── build.js
│   └── release.js
├── .github/                # GitHub configuration
│   ├── workflows/
│   │   └── ci.yml
│   └── CONTRIBUTING.md
├── .eslintrc.json         # ESLint config
├── .prettierrc            # Prettier config
├── jest.config.js         # Jest config
├── tsconfig.json          # TypeScript config
├── rollup.config.js       # Bundler config
└── package.json

Initializing Your Package

Step 1: Create Project Directory

mkdir string-manipulator
cd string-manipulator

Step 2: Initialize package.json

npm init
You’ll be prompted for:
  • package name: string-manipulator
  • version: 1.0.0
  • description: A lightweight utility for advanced string manipulation
  • entry point: index.js
  • test command: jest
  • git repository: https://github.com/yourusername/string-manipulator
  • keywords: string, utility, manipulation, text
  • author: Your Name your.email@example.com
  • license: MIT

Step 3: Complete package.json

Every field here serves a purpose. The ones people skip are often the ones that matter most for discoverability and trust:
{
  "name": "string-manipulator",
  "version": "1.0.0",
  "description": "A lightweight utility for advanced string manipulation",
  "main": "index.js",
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "lint": "eslint src/",
    "lint:fix": "eslint src/ --fix",
    "format": "prettier --write \"src/**/*.js\"",
    "prepublishOnly": "npm test && npm run lint"
  },
  "keywords": [
    "string",
    "utility",
    "manipulation",
    "text",
    "format"
  ],
  "author": "Your Name <your.email@example.com>",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/yourusername/string-manipulator"
  },
  "bugs": {
    "url": "https://github.com/yourusername/string-manipulator/issues"
  },
  "homepage": "https://github.com/yourusername/string-manipulator#readme",
  "engines": {
    "node": ">=14.0.0"
  },
  "files": [
    "index.js",
    "src/",
    "README.md",
    "LICENSE"
  ],
  "devDependencies": {
    "eslint": "^8.50.0",
    "jest": "^29.7.0",
    "prettier": "^3.0.3"
  },
  "dependencies": {}
}
A few fields deserve extra attention:
  • "files" — This is a whitelist of what gets published. Without it, NPM publishes everything not in .npmignore. Using "files" is safer because it is explicit: you list what IS included rather than trying to list everything that is NOT. This prevents accidentally publishing test/, .env, or other sensitive files.
  • "engines" — Declares the minimum Node.js version. If a user tries to install your package on Node 12 and you require Node 14+, NPM will warn them. Without this field, they install successfully but get a confusing runtime error.
  • "prepublishOnly" — This lifecycle hook runs before npm publish and blocks publishing if tests or linting fail. Think of it as a pre-commit hook for your package registry. It has saved countless developers from publishing broken code.

Writing Package Code

Example: String Manipulator Package

src/index.js (Main Entry Point)

/**
 * String Manipulator - A utility library for advanced string operations
 * @module string-manipulator
 */

/**
 * Converts a string to title case
 * @param {string} str - The input string
 * @returns {string} The title-cased string
 * @example
 * toTitleCase('hello world') // 'Hello World'
 */
function toTitleCase(str) {
  // Defensive type checking -- public APIs should never silently coerce types.
  // Throwing a TypeError (not a generic Error) follows Node.js conventions
  // and helps consumers debug issues in their calling code.
  if (typeof str !== 'string') {
    throw new TypeError('Expected a string');
  }

  return str
    .toLowerCase()              // Normalize first: "HELLO WORLD" -> "hello world"
    .split(' ')                 // Split into words by space
    .map(word => word.charAt(0).toUpperCase() + word.slice(1))  // Capitalize first letter
    .join(' ');                 // Rejoin into a single string
}

/**
 * Converts a string to camelCase
 * @param {string} str - The input string
 * @returns {string} The camelCased string
 * @example
 * toCamelCase('hello world') // 'helloWorld'
 */
function toCamelCase(str) {
  if (typeof str !== 'string') {
    throw new TypeError('Expected a string');
  }

  return str
    .toLowerCase()
    // This regex finds any non-alphanumeric character(s) followed by a letter,
    // and replaces the whole match with just the uppercase letter.
    // "hello-world" -> match "-w" -> replace with "W" -> "helloWorld"
    .replace(/[^a-zA-Z0-9]+(.)/g, (_, char) => char.toUpperCase());
}

/**
 * Converts a string to snake_case
 * @param {string} str - The input string
 * @returns {string} The snake_cased string
 * @example
 * toSnakeCase('Hello World') // 'hello_world'
 */
function toSnakeCase(str) {
  if (typeof str !== 'string') {
    throw new TypeError('Expected a string');
  }

  return str
    .replace(/([A-Z])/g, '_$1')
    .toLowerCase()
    .replace(/\s+/g, '_')
    .replace(/^_/, '');
}

/**
 * Converts a string to kebab-case
 * @param {string} str - The input string
 * @returns {string} The kebab-cased string
 * @example
 * toKebabCase('Hello World') // 'hello-world'
 */
function toKebabCase(str) {
  if (typeof str !== 'string') {
    throw new TypeError('Expected a string');
  }

  return str
    .replace(/([A-Z])/g, '-$1')
    .toLowerCase()
    .replace(/\s+/g, '-')
    .replace(/^-/, '');
}

/**
 * Truncates a string to a specified length
 * @param {string} str - The input string
 * @param {number} length - Maximum length
 * @param {string} [suffix='...'] - Suffix to append if truncated
 * @returns {string} The truncated string
 * @example
 * truncate('Hello World', 8) // 'Hello...'
 */
function truncate(str, length, suffix = '...') {
  if (typeof str !== 'string') {
    throw new TypeError('Expected a string');
  }

  // If the string is already short enough, return it unchanged.
  // This avoids the edge case where slicing + suffix makes it longer.
  if (str.length <= length) {
    return str;
  }

  // Subtract the suffix length so the total output fits within `length`.
  // "Hello World" truncated to 8 with "..." -> "Hello" + "..." = "Hello..." (8 chars)
  return str.slice(0, length - suffix.length) + suffix;
}

/**
 * Reverses a string
 * @param {string} str - The input string
 * @returns {string} The reversed string
 * @example
 * reverse('hello') // 'olleh'
 */
function reverse(str) {
  if (typeof str !== 'string') {
    throw new TypeError('Expected a string');
  }

  // Note: This simple approach breaks with Unicode characters like emojis
  // or multi-byte characters (e.g., "hello 🌍" reversed incorrectly).
  // For production use, consider Array.from(str).reverse().join('')
  // which handles Unicode code points correctly.
  return str.split('').reverse().join('');
}

/**
 * Counts the words in a string
 * @param {string} str - The input string
 * @returns {number} The word count
 * @example
 * wordCount('Hello world') // 2
 */
function wordCount(str) {
  if (typeof str !== 'string') {
    throw new TypeError('Expected a string');
  }

  // trim() removes leading/trailing whitespace to avoid empty matches.
  // split(/\s+/) splits on one or more whitespace characters (handles tabs, newlines, double spaces).
  // filter(Boolean) removes empty strings from the array (e.g., if the input was all whitespace).
  return str.trim().split(/\s+/).filter(Boolean).length;
}

/**
 * Capitalizes the first letter of a string
 * @param {string} str - The input string
 * @returns {string} The capitalized string
 * @example
 * capitalize('hello') // 'Hello'
 */
function capitalize(str) {
  if (typeof str !== 'string') {
    throw new TypeError('Expected a string');
  }

  return str.charAt(0).toUpperCase() + str.slice(1);
}

// Export all functions
module.exports = {
  toTitleCase,
  toCamelCase,
  toSnakeCase,
  toKebabCase,
  truncate,
  reverse,
  wordCount,
  capitalize
};

Supporting Multiple Export Formats

For maximum compatibility, support both CommonJS and ES Modules. This is one of the most confusing parts of the Node.js ecosystem, so let me be clear about why it matters: older projects and most server-side code use require() (CommonJS). Modern frontend tooling, newer Node.js projects, and browser bundlers expect import/export (ES Modules). If your package only supports one format, you lose half your potential users.

index.js (Dual Export)

// Your functions here...

// CommonJS export
module.exports = {
  toTitleCase,
  toCamelCase,
  // ... other functions
};

// ES Module export (if supported)
if (typeof exports !== 'undefined') {
  exports.toTitleCase = toTitleCase;
  exports.toCamelCase = toCamelCase;
  // ... other exports
}

package.json (ES Module Support)

{
  "name": "string-manipulator",
  "version": "1.0.0",
  "main": "./dist/index.cjs",       // Entry point for require() -- CommonJS consumers
  "module": "./dist/index.mjs",      // Entry point for bundlers (Webpack, Rollup) -- enables tree-shaking
  "exports": {
    ".": {
      "require": "./dist/index.cjs", // When someone does: const x = require('string-manipulator')
      "import": "./dist/index.mjs",  // When someone does: import x from 'string-manipulator'
      "types": "./dist/index.d.ts"   // TypeScript type definitions -- resolved automatically
    }
  },
  "type": "module"                   // Tells Node.js to treat .js files as ES Modules by default
}
Practical approach for 2025+: If you are starting a new package today, write in ES Module syntax (import/export) and use a build tool like tsup or unbuild to generate both CJS and ESM outputs. These tools handle the dual-format complexity for you in a single config file, so you do not have to manually manage two entry points.

Testing Your Package

Setting Up Jest

npm install --save-dev jest

test/index.test.js

const {
  toTitleCase,
  toCamelCase,
  toSnakeCase,
  toKebabCase,
  truncate,
  reverse,
  wordCount,
  capitalize
} = require('../src/index');

describe('String Manipulator', () => {
  describe('toTitleCase', () => {
    it('should convert string to title case', () => {
      expect(toTitleCase('hello world')).toBe('Hello World');
      expect(toTitleCase('THE QUICK BROWN FOX')).toBe('The Quick Brown Fox');
    });

    it('should handle single word', () => {
      expect(toTitleCase('hello')).toBe('Hello');
    });

    it('should throw error for non-string input', () => {
      expect(() => toTitleCase(123)).toThrow(TypeError);
      expect(() => toTitleCase(null)).toThrow(TypeError);
    });
  });

  describe('toCamelCase', () => {
    it('should convert string to camelCase', () => {
      expect(toCamelCase('hello world')).toBe('helloWorld');
      expect(toCamelCase('the-quick-brown-fox')).toBe('theQuickBrownFox');
    });

    it('should handle already camelCased strings', () => {
      expect(toCamelCase('helloWorld')).toBe('helloworld');
    });
  });

  describe('toSnakeCase', () => {
    it('should convert string to snake_case', () => {
      expect(toSnakeCase('Hello World')).toBe('hello_world');
      expect(toSnakeCase('theQuickBrownFox')).toBe('the_quick_brown_fox');
    });
  });

  describe('toKebabCase', () => {
    it('should convert string to kebab-case', () => {
      expect(toKebabCase('Hello World')).toBe('hello-world');
      expect(toKebabCase('theQuickBrownFox')).toBe('the-quick-brown-fox');
    });
  });

  describe('truncate', () => {
    it('should truncate string to specified length', () => {
      expect(truncate('Hello World', 8)).toBe('Hello...');
      expect(truncate('Hello', 10)).toBe('Hello');
    });

    it('should use custom suffix', () => {
      expect(truncate('Hello World', 8, '…')).toBe('Hello W…');
    });
  });

  describe('reverse', () => {
    it('should reverse a string', () => {
      expect(reverse('hello')).toBe('olleh');
      expect(reverse('12345')).toBe('54321');
    });
  });

  describe('wordCount', () => {
    it('should count words in a string', () => {
      expect(wordCount('Hello world')).toBe(2);
      expect(wordCount('The quick brown fox')).toBe(4);
    });

    it('should handle extra spaces', () => {
      expect(wordCount('  hello   world  ')).toBe(2);
    });
  });

  describe('capitalize', () => {
    it('should capitalize first letter', () => {
      expect(capitalize('hello')).toBe('Hello');
      expect(capitalize('world')).toBe('World');
    });
  });
});

Running Tests

# Run tests once
npm test

# Run tests in watch mode
npm run test:watch

# Generate coverage report
npm run test:coverage

Coverage Report Example

-----------------|---------|----------|---------|---------|-------------------
File             | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------|---------|----------|---------|---------|-------------------
All files        |     100 |      100 |     100 |     100 |
 index.js        |     100 |      100 |     100 |     100 |
-----------------|---------|----------|---------|---------|-------------------

Documentation

README.md Structure

# String Manipulator

> A lightweight, zero-dependency utility library for advanced string manipulation in Node.js and browsers.

[![NPM Version](https://img.shields.io/npm/v/string-manipulator.svg)](https://www.npmjs.com/package/string-manipulator)
[![Build Status](https://github.com/yourusername/string-manipulator/workflows/CI/badge.svg)](https://github.com/yourusername/string-manipulator/actions)
[![Coverage](https://img.shields.io/codecov/c/github/yourusername/string-manipulator)](https://codecov.io/gh/yourusername/string-manipulator)
[![License](https://img.shields.io/npm/l/string-manipulator.svg)](LICENSE)

## Features

✅ Zero dependencies
✅ Lightweight (< 2KB gzipped)
✅ TypeScript support
✅ Full test coverage
✅ Works in Node.js and browsers
✅ ESM and CommonJS support

## Installation

```bash
npm install string-manipulator
Or with Yarn:
yarn add string-manipulator

Usage

const { toTitleCase, toCamelCase, truncate } = require('string-manipulator');

// Title Case
toTitleCase('hello world'); // 'Hello World'

// Camel Case
toCamelCase('hello world'); // 'helloWorld'

// Truncate
truncate('Hello World', 8); // 'Hello...'

API

toTitleCase(str)

Converts a string to Title Case. Parameters:
  • str (string): The input string
Returns: string Example:
toTitleCase('hello world'); // 'Hello World'

toCamelCase(str)

Converts a string to camelCase. Parameters:
  • str (string): The input string
Returns: string Example:
toCamelCase('hello world'); // 'helloWorld'
[… continue with all other functions …]

Browser Usage

<script src="https://unpkg.com/string-manipulator"></script>
<script>
  console.log(StringManipulator.toTitleCase('hello world'));
</script>

TypeScript

import { toTitleCase, toCamelCase } from 'string-manipulator';

const title: string = toTitleCase('hello world');

Contributing

Contributions are welcome! Please read CONTRIBUTING.md for details.

License

MIT © Your Name

Changelog

See CHANGELOG.md for version history.

### CHANGELOG.md

```markdown
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.0.0] - 2024-01-15

### Added
- Initial release
- `toTitleCase()` function
- `toCamelCase()` function
- `toSnakeCase()` function
- `toKebabCase()` function
- `truncate()` function
- `reverse()` function
- `wordCount()` function
- `capitalize()` function

### Changed
- Nothing

### Fixed
- Nothing

## [0.1.0] - 2024-01-10

### Added
- Beta release
- Basic string manipulation functions

Configuring Package Files

.gitignore

# Dependencies
node_modules/

# Test coverage
coverage/

# Build output
dist/
*.log

# Environment
.env
.env.local

# IDE
.vscode/
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

.npmignore

# Source files (if you build to dist/)
src/
test/

# Development files
.github/
.vscode/
coverage/
*.test.js
jest.config.js
.eslintrc.json
.prettierrc

# Documentation (keep README!)
docs/
examples/

# Git
.git/
.gitignore
If .npmignore exists, it overrides .gitignore. Make sure not to accidentally exclude important files!

LICENSE (MIT Example)

MIT License

Copyright (c) 2024 Your Name

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Testing Locally

Test your package locally before publishing. npm link creates a symlink from the global node_modules to your local package directory, so changes you make to the package are reflected immediately in any project that links to it. Think of it as a live development connection between your package source and your test project.
# Step 1: In your package directory -- register this package as a global link
npm link
# This creates a symlink: global node_modules/string-manipulator -> your local directory

# Step 2: In your test project -- connect to the linked package
npm link string-manipulator
# This creates a symlink: test-project/node_modules/string-manipulator -> global link -> your source

# Step 3: Now changes in your package source are instantly available
const { toTitleCase } = require('string-manipulator');
# Edits to src/index.js take effect immediately -- no reinstall needed
Common npm link pitfall: If your package and your test project use different versions of a shared dependency (like React), the symlink can cause “duplicate module” errors because Node.js resolves the dependency from the package’s node_modules (not the test project’s). Use npm pack and install the tarball for final pre-publish testing to avoid this issue.

Using Local Path

# In your test project
npm install ../path/to/string-manipulator

# Or in package.json
{
  "dependencies": {
    "string-manipulator": "file:../string-manipulator"
  }
}

Test in Isolation

Create a test directory to verify your package works as expected:
mkdir test-package
cd test-package
npm init -y
npm install ../string-manipulator
// test.js
const { toTitleCase } = require('string-manipulator');

console.log(toTitleCase('hello world')); // Should output: Hello World

Version Management

Semantic Versioning (SemVer)

Format: MAJOR.MINOR.PATCH — this is not just a convention, it is a contract with your users. When someone writes "string-manipulator": "^1.2.0" in their package.json, the ^ means “give me any version 1.x.x that is >= 1.2.0.” They are trusting you to not break their code in a minor or patch release.
  • MAJOR: Breaking changes — you renamed a function, removed a parameter, changed return types (1.0.0 to 2.0.0). Consumers must update their code.
  • MINOR: New features that are backward-compatible — you added a new function, added an optional parameter (1.0.0 to 1.1.0). Existing code keeps working.
  • PATCH: Bug fixes that are backward-compatible — you fixed a regex edge case, improved performance (1.0.0 to 1.0.1). No API changes at all.
The hardest SemVer question: Is fixing a bug that people accidentally depend on a breaking change? For example, if your toTitleCase function incorrectly handles hyphens and someone’s code relies on that incorrect behavior, fixing it could break their code. Technically this is a bug fix (patch), but in practice, it can break consumers. The pragmatic approach: fix it as a minor version bump and document the behavioral change in your CHANGELOG. Reserve major bumps for intentional API redesigns.

Updating Versions

# Patch version (1.0.0 → 1.0.1)
npm version patch

# Minor version (1.0.0 → 1.1.0)
npm version minor

# Major version (1.0.0 → 2.0.0)
npm version major

# Specific version
npm version 1.2.3

# Pre-release versions
npm version prepatch  # 1.0.0 → 1.0.1-0
npm version preminor  # 1.0.0 → 1.1.0-0
npm version premajor  # 1.0.0 → 2.0.0-0

Version Lifecycle Scripts

{
  "scripts": {
    "preversion": "npm test",
    "version": "npm run build && git add -A dist",
    "postversion": "git push && git push --tags"
  }
}

Publishing Your Package

Step 1: Create NPM Account

# Sign up at npmjs.com first, then:
npm login

# Verify login
npm whoami

Step 2: Prepare for Publishing

# Run tests -- never publish without passing tests
npm test

# CRITICAL: Check what will be published BEFORE actually publishing
# This shows you every file that will be in the published package
npm pack --dry-run

# Create an actual tarball -- you can inspect it and install it locally
npm pack
# This creates string-manipulator-1.0.0.tgz
# Install it in a test project: npm install ./string-manipulator-1.0.0.tgz
Always run npm pack --dry-run before your first publish. This is your safety net against publishing .env files, node_modules, test fixtures with credentials, or your entire Git history. Review the file list carefully. If you see anything unexpected, adjust your "files" field in package.json or add entries to .npmignore.

Step 3: Publish

# Public package
npm publish

# Scoped package (public)
npm publish --access public

# Test run (doesn't actually publish)
npm publish --dry-run

Step 4: Verify Publication

# Check package info
npm view string-manipulator

# Install and test
npm install string-manipulator

Publishing Scoped Packages

Public Scoped Package

# Package name: @username/string-manipulator
npm publish --access public

Private Scoped Package

# Requires paid NPM account
npm publish --access restricted

package.json for Scoped Package

{
  "name": "@yourusername/string-manipulator",
  "version": "1.0.0",
  "publishConfig": {
    "access": "public"
  }
}

Continuous Integration

GitHub Actions Workflow

CI/CD is non-negotiable for a package that other people depend on. A green CI badge on your README tells potential users “this person takes quality seriously.” Here is a production-ready workflow: Create .github/workflows/ci.yml:
name: CI

on:
  push:
    branches: [ main ]          # Run on every push to main
  pull_request:
    branches: [ main ]          # Run on PRs targeting main

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        # Test against every Node.js version you claim to support in "engines"
        # This catches compatibility issues before your users do
        node-version: [14.x, 16.x, 18.x, 20.x]

    steps:
    - uses: actions/checkout@v3

    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v3
      with:
        node-version: ${{ matrix.node-version }}

    - name: Install dependencies
      run: npm ci              # Use 'ci' (not 'install') for reproducible installs from lockfile

    - name: Run linter
      run: npm run lint        # Catch style issues and potential bugs

    - name: Run tests
      run: npm test            # Your core quality gate

    - name: Generate coverage
      run: npm run test:coverage  # Track that test coverage does not regress

    - name: Upload coverage
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/coverage-final.json

Automated Publishing

Create .github/workflows/publish.yml:
name: Publish Package

on:
  release:
    types: [created]

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - uses: actions/setup-node@v3
      with:
        node-version: '18.x'
        registry-url: 'https://registry.npmjs.org'

    - run: npm ci
    - run: npm test
    - run: npm publish
      env:
        NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Advanced Package Features

Adding TypeScript Definitions

Even if your package is written in JavaScript, provide TypeScript definitions:

index.d.ts

/**
 * Converts a string to title case
 */
export function toTitleCase(str: string): string;

/**
 * Converts a string to camelCase
 */
export function toCamelCase(str: string): string;

/**
 * Converts a string to snake_case
 */
export function toSnakeCase(str: string): string;

/**
 * Converts a string to kebab-case
 */
export function toKebabCase(str: string): string;

/**
 * Truncates a string to specified length
 */
export function truncate(str: string, length: number, suffix?: string): string;

/**
 * Reverses a string
 */
export function reverse(str: string): string;

/**
 * Counts words in a string
 */
export function wordCount(str: string): number;

/**
 * Capitalizes first letter
 */
export function capitalize(str: string): string;

package.json

{
  "types": "./index.d.ts",
  "typings": "./index.d.ts"
}

CLI Support

Make your package executable from command line:

bin/cli.js

#!/usr/bin/env node

const { toTitleCase, toCamelCase } = require('../src/index');
const args = process.argv.slice(2);

if (args.length === 0) {
  console.log('Usage: string-manipulator <command> <text>');
  console.log('Commands: title, camel, snake, kebab');
  process.exit(1);
}

const [command, ...textParts] = args;
const text = textParts.join(' ');

switch (command) {
  case 'title':
    console.log(toTitleCase(text));
    break;
  case 'camel':
    console.log(toCamelCase(text));
    break;
  default:
    console.log('Unknown command:', command);
    process.exit(1);
}

package.json

{
  "bin": {
    "string-manipulator": "./bin/cli.js"
  }
}

Usage

# After npm install -g string-manipulator
string-manipulator title "hello world"
# Output: Hello World

Peer Dependencies

For plugins or extensions that require a host package:
{
  "peerDependencies": {
    "express": ">=4.0.0"
  },
  "peerDependenciesMeta": {
    "express": {
      "optional": false
    }
  }
}

Optional Dependencies

For packages that enhance functionality but aren’t required:
{
  "optionalDependencies": {
    "colors": "^1.4.0"
  }
}

Package Maintenance

Updating Your Package

# Make changes to code
# Update tests
npm test

# Update version
npm version patch

# Publish update
npm publish

Deprecating Versions

# Deprecate specific version
npm deprecate string-manipulator@1.0.0 "Please upgrade to 1.0.1"

# Deprecate all versions
npm deprecate string-manipulator "Package is no longer maintained"

Unpublishing Packages

# Unpublish specific version (within 72 hours)
npm unpublish string-manipulator@1.0.0

# Unpublish entire package (within 72 hours, if no dependents)
npm unpublish string-manipulator --force
Unpublishing is discouraged as it breaks projects depending on your package. Use deprecation instead.

Security Best Practices

1. Validate Input

function toTitleCase(str) {
  // Always validate input
  if (typeof str !== 'string') {
    throw new TypeError('Expected a string');
  }

  // Sanitize if needed
  str = str.trim();

  // Implement function logic
  // ...
}

2. Avoid eval() and Function()

// BAD - Never do this
function execute(code) {
  eval(code);
}

// GOOD - Use safe alternatives
function execute(data) {
  return JSON.parse(data);
}

3. Keep Dependencies Minimal

Every dependency you add is a liability. It is code you did not write, cannot fully control, and must keep updated. The infamous left-pad incident (2016) showed how a single 11-line package being unpublished broke thousands of builds globally. The event-stream incident (2018) showed how a dependency can be hijacked to inject malicious code.
# Regularly audit dependencies for known vulnerabilities
npm audit

# Fix vulnerabilities automatically (patches and minor versions only)
npm audit fix

# Check for outdated packages -- old dependencies accumulate vulnerabilities
npm outdated

# See why a dependency is in your tree (often transitive deps are the issue)
npm explain <package-name>
The zero-dependency ideal: For utility packages, aim for zero runtime dependencies. If you need a small function from a large library, consider inlining it (with attribution). Consumers will trust a package with zero dependencies far more than one with a deep dependency tree. Libraries like date-fns gained market share over moment.js partly because they are tree-shakeable with minimal transitive dependencies.

4. Use .npmignore

Prevent sensitive files from being published:
.env
.env.*
config/secrets.json
private/
*.key
*.pem

5. Enable 2FA on NPM

# Enable two-factor authentication
npm profile enable-2fa auth-and-writes

Performance Optimization

Bundle Size Optimization

// Use tree-shaking friendly exports
export function toTitleCase(str) { /* ... */ }
export function toCamelCase(str) { /* ... */ }

// Instead of
module.exports = { toTitleCase, toCamelCase };

Minimize Dependencies

{
  "dependencies": {
    // Keep this list as small as possible
  },
  "devDependencies": {
    // Tools only needed for development
  }
}

Use Lazy Loading

// Load heavy dependencies only when needed
function complexOperation() {
  const heavyLib = require('heavy-library');
  return heavyLib.process();
}

Marketing Your Package

1. Write Great Documentation

  • Clear README with examples
  • API documentation
  • Usage examples
  • Troubleshooting guide

2. Add Badges

[![NPM Version](https://img.shields.io/npm/v/string-manipulator.svg)](https://npmjs.com/package/string-manipulator)
[![Downloads](https://img.shields.io/npm/dm/string-manipulator.svg)](https://npmjs.com/package/string-manipulator)
[![Build Status](https://github.com/user/repo/workflows/CI/badge.svg)](https://github.com/user/repo/actions)
[![Coverage](https://img.shields.io/codecov/c/github/user/repo)](https://codecov.io/gh/user/repo)

3. Choose Good Keywords

{
  "keywords": [
    "string",
    "text",
    "manipulation",
    "utility",
    "format",
    "convert",
    "camelcase",
    "kebabcase"
  ]
}

4. Create Examples

examples/
├── basic-usage.js
├── advanced-usage.js
└── browser-usage.html

5. Share on Social Media

  • Tweet about your package
  • Post on Reddit (r/javascript, r/node)
  • Write blog post
  • Create demo on CodeSandbox

Common Pitfalls

1. Not Testing Before Publishing

Always run tests before publishing:
{
  "scripts": {
    "prepublishOnly": "npm test && npm run lint"
  }
}

2. Including Unnecessary Files

Use files field or .npmignore:
{
  "files": [
    "index.js",
    "src/",
    "README.md",
    "LICENSE"
  ]
}

3. Breaking Changes Without Major Version

Follow semantic versioning strictly. If you rename a function from toTitle() to toTitleCase() in a minor release, everyone who uses toTitle() gets a broken build on their next npm install. They will not be happy, and they will not trust your package again. When in doubt about whether something is “breaking,” err on the side of a major version bump.

4. No TypeScript Definitions

Always provide TypeScript definitions for better developer experience, even if your package is written in plain JavaScript. TypeScript has become the default for many teams, and without type definitions, your package shows up as any in their editor — no autocomplete, no parameter hints, no documentation on hover. This alone is enough for many developers to choose a competing package.

5. Poor Error Messages

// BAD
throw new Error('Invalid');

// GOOD
throw new TypeError(`Expected string, received ${typeof input}`);

Real-World Example: Complete Package

Let’s look at a complete, production-ready package structure:

Final Directory Structure

string-manipulator/
├── src/
│   ├── index.js
│   ├── converters.js
│   └── validators.js
├── test/
│   ├── index.test.js
│   ├── converters.test.js
│   └── validators.test.js
├── examples/
│   ├── basic.js
│   └── advanced.js
├── bin/
│   └── cli.js
├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── publish.yml
├── index.d.ts
├── .gitignore
├── .npmignore
├── .eslintrc.json
├── jest.config.js
├── LICENSE
├── README.md
├── CHANGELOG.md
├── CONTRIBUTING.md
└── package.json

Complete package.json

{
  "name": "string-manipulator",
  "version": "1.0.0",
  "description": "A lightweight utility for advanced string manipulation",
  "main": "src/index.js",
  "types": "index.d.ts",
  "bin": {
    "string-manipulator": "./bin/cli.js"
  },
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage",
    "lint": "eslint src/ test/",
    "lint:fix": "eslint src/ test/ --fix",
    "format": "prettier --write \"**/*.{js,json,md}\"",
    "prepublishOnly": "npm test && npm run lint",
    "preversion": "npm test",
    "postversion": "git push && git push --tags"
  },
  "keywords": [
    "string",
    "text",
    "manipulation",
    "utility",
    "format",
    "camelcase",
    "kebabcase",
    "snakecase"
  ],
  "author": "Your Name <your.email@example.com>",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/yourusername/string-manipulator.git"
  },
  "bugs": {
    "url": "https://github.com/yourusername/string-manipulator/issues"
  },
  "homepage": "https://github.com/yourusername/string-manipulator#readme",
  "engines": {
    "node": ">=14.0.0"
  },
  "files": [
    "src/",
    "bin/",
    "index.d.ts",
    "README.md",
    "LICENSE"
  ],
  "devDependencies": {
    "@types/node": "^20.8.0",
    "eslint": "^8.50.0",
    "jest": "^29.7.0",
    "prettier": "^3.0.3"
  },
  "dependencies": {}
}

Summary

Building an NPM package involves:
  1. Planning: Define purpose, name, and API
  2. Structure: Organize code logically
  3. Development: Write clean, tested code
  4. Documentation: Create comprehensive README
  5. Testing: Achieve high test coverage
  6. Configuration: Set up package.json correctly
  7. Publishing: Share with the community
  8. Maintenance: Keep package updated and secure
Start small, iterate, and gather feedback. Your first package doesn’t need to be perfect—shipping is more important than perfection!

Interview Deep-Dive

Strong Answer:“All my tests pass” is a necessary but insufficient signal. The first thing I need to understand is what broke and in whose environment. I ask the user for their Node.js version, module system (CommonJS vs ESM), package manager (npm, yarn, pnpm — they resolve dependency trees differently), and whether they are using the package in Node.js or a bundler like Webpack or Vite.Then I verify what actually shipped. I run npm pack --dry-run against my 1.2.0 tag and compare the file list to 1.1.0. The most common cause of “works locally, breaks for users” is a file missing from the published tarball. If I added a new internal module in src/helpers/newUtil.js and import it from index.js, but my "files" whitelist in package.json does not include it, my tests pass (they run against the source tree) but the published package is missing the file. The user sees MODULE_NOT_FOUND.If the tarball is correct, I check for behavioral breaking changes. Maybe a function that returned null for empty input now returns undefined, or an error that used to throw Error now throws TypeError. These are breaking changes that tests might not catch if they only assert on the happy path. I diff the code between 1.1.0 and 1.2.0 and look for any change to function signatures, return types, error types, or edge case behavior.Third possibility: a dependency resolution issue. If I updated a sub-dependency’s version range in my package.json, the user might get a different resolved version than I tested against. Their lockfile pins one version; my CI resolves another. This is why running npm ci (which uses the lockfile exactly) in CI is critical, and why publishing with as few dependencies as possible reduces this risk.For the fix: if it is genuinely a breaking change, I publish a 1.2.1 patch that reverts the behavior, deprecate 1.2.0 with a message pointing to 1.2.1, and if the original change was intentional, I queue it for a proper 2.0.0 major release with migration notes in the changelog. The rule is absolute: never re-publish the same version number with different contents. Lockfiles depend on version immutability.Going forward, I add a CI step that runs npm pack, installs the resulting tarball into a fresh project, and executes the README’s quick-start example. This catches tarball-level issues that unit tests against the source tree never will.
Strong Answer:These three fields control when and how a package’s dependencies are installed, and confusing them causes real production bugs.dependencies are installed whenever anyone installs your package. If my package lists lodash in dependencies, then npm install my-package also installs lodash into the consumer’s node_modules. These are runtime requirements — code that executes when the user calls your functions.devDependencies are installed only during development of your package. They are NOT installed when a consumer runs npm install my-package. Jest, ESLint, Prettier, build tools — everything needed to develop but not to run the package goes here.peerDependencies declare “I need this package at runtime, but the consumer must provide it.” They prevent duplicate installations of packages that must exist as a single instance. This is the critical one.The production bug scenario: suppose I am building an Express middleware package called express-request-logger. I mistakenly put express in dependencies instead of peerDependencies. When a user installs my package, npm installs a second, private copy of Express nested inside node_modules/express-request-logger/node_modules/express. Now there are two Express instances in memory. My middleware calls app.use() on one instance; the user’s routes are registered on the other instance. The middleware never fires because it is attached to a different Express application object than the one handling requests. The user sees zero logging and gets no error message — just silent failure that is extremely difficult to diagnose.The fix is straightforward: Express goes in peerDependencies. This tells npm to use the consumer’s already-installed copy of Express. My middleware’s require('express') resolves to the same singleton instance the user is using. The same pattern applies to React (two React instances break hooks), Webpack plugins, and any framework that relies on global or singleton state.A related mistake I see frequently: putting a build tool like TypeScript or Babel in dependencies instead of devDependencies. The package should ship compiled JavaScript. Putting the compiler in dependencies forces every consumer to download it (TypeScript is roughly 50MB), bloating their node_modules and npm install time for something they never use.
Strong Answer:This is a coordination problem as much as a technical one. The key constraint is that 15 services cannot all migrate simultaneously, so I need a transition period where both the old and new APIs work.Step one: I ship the new API alongside the old one in a minor version, say 2.3.0. The new functions are added, the old functions continue to work exactly as before. No breaking change yet. I add deprecation warnings to the old functions using process.emitWarning() or console.warn() on first call, so teams see them in their logs and know migration is coming. The warning message includes the replacement function name and a link to a migration guide.Step two: I write a migration guide as a markdown file in the package repo. It covers every changed function, shows before-and-after code, and explains the rationale for the change. I also write a codemod using jscodeshift if the change is mechanical enough to automate. For 15 services, a codemod saves hundreds of person-hours of manual find-and-replace.Step three: I set a migration deadline — typically 4 to 6 weeks for an internal package. I communicate this via Slack, the package’s CHANGELOG, and ideally a brief message in each team’s standup channel. I make myself available for questions and offer to pair on tricky migrations.Step four: after the deadline, I check dependency graphs to see which services still pin the old version. For any that have not migrated, I reach out directly rather than breaking them. Only when all 15 services have confirmed migration (or explicitly accepted the risk of pinning the old version) do I publish the major version bump (3.0.0) that removes the deprecated API.Step five: the major version is published. Services using ^2.x in their package.json are not affected because semver ranges do not auto-upgrade across major versions. They continue using 2.3.x until they explicitly opt into 3.0.0.The tooling that makes this manageable: a monorepo tool like Nx or Turborepo if the services are in the same repository (run the codemod once across all projects), or Renovate/Dependabot configured to auto-create PRs when the new major version is published. The anti-pattern is publishing a major version with no migration path and hoping teams figure it out — that erodes trust in the internal package and teams start copying the code locally instead of depending on it.
Strong Answer:package-lock.json records the exact resolved version of every package in your dependency tree — not just your direct dependencies, but their dependencies, and their dependencies’ dependencies, all the way down. When you run npm install, npm reads your package.json for the version ranges (for example "lodash": "^4.17.0") and resolves them to specific versions (for example 4.17.21). The lockfile stores those resolved versions so that every subsequent install reproduces the exact same tree.Without the lockfile, npm install resolves version ranges fresh every time. If lodash publishes 4.17.22 between when you installed and when your colleague installs, you get different versions. This is the “works on my machine” class of bugs — code that passes all tests on your laptop fails in CI or on a teammate’s machine because a transitive dependency resolved to a different patch version with a subtle behavior change.The practical impact of deleting it: your next npm install resolves all ranges from scratch, potentially pulling in newer versions of every dependency. If any of those newer versions have bugs or breaking changes (even in patch versions — maintainers make mistakes), your previously working project breaks. In CI, this is especially dangerous because every build might resolve to different versions, making failures non-reproducible.For application projects (services, apps), you should always commit the lockfile. This ensures that npm ci in CI installs the exact versions you tested against. npm ci is different from npm install — it deletes node_modules entirely and installs strictly from the lockfile, failing if the lockfile does not match package.json. This is the correct command for CI pipelines.For library packages (NPM packages you publish), the convention is more nuanced. You should still commit the lockfile for reproducible development and CI, but consumers of your package never see your lockfile — npm ignores it when installing your package as a dependency. This means your library’s dependencies are resolved by the consumer’s npm install using the ranges in your package.json. This is by design: it allows the consumer’s lockfile to be the single source of truth for the entire dependency tree.The worst anti-pattern: adding package-lock.json to .gitignore. Teams sometimes do this because merge conflicts in the lockfile are annoying. But the fix is to use npm install --package-lock-only to regenerate the lockfile after resolving conflicts, not to eliminate deterministic builds entirely. Non-deterministic dependency resolution is far more expensive to debug than merge conflicts.

Next Steps

After publishing your package:
  1. Monitor GitHub issues and respond to users
  2. Keep dependencies updated
  3. Release patches for bugs promptly
  4. Consider feature requests carefully
  5. Build a community around your package
  6. Document breaking changes clearly
  7. Celebrate your contribution to open source