DEV Community

Alex Chen
Alex Chen

Posted on

I Published My First npm Package — Here's Everything I Wish I Knew

I Published My First npm Package — Here's Everything I Wish I Knew

From idea to published package in one guide.

Before You Start

# Check if your package name is taken
npm view your-package-name
# If it returns 404, the name is available!

# Or search:
npm search your-package-name
Enter fullscreen mode Exit fullscreen mode

Project Setup

mkdir my-awesome-package && cd my-awesome-package
npm init -y

# Essential files to create:
touch README.md
touch .gitignore
touch .npmignore
Enter fullscreen mode Exit fullscreen mode

package.json — The Most Important File

{
  "name": "@username/my-awesome-package", // Scoped packages are recommended!
  "version": "1.0.0",
  "description": "A brief description of what this does",
  "main": "./dist/index.js",             // Entry point
  "types": ./dist/index.d.ts",           // TypeScript types (important!)
  "module": "./dist/index.mjs",          // ESM support
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    }
  },
  "files": ["dist"],                     // Only publish dist folder
  "scripts": {
    "build": "tsc",
    "prepublishOnly": "npm run build",   // Build before publishing!
    "test": "jest",
    "lint": "eslint src/"
  },
  "keywords": ["utility", "javascript"],
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/your-username/my-awesome-package.git"
  },
  "engines": {
    "node": ">=16.0.0"
  },
  "peerDependencies": {
    "react": ">=17.0.0"                  // Don't install, just require
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "jest": "^29.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

.npmignore — What NOT to Publish

# .npmignore
src/
test/
*.ts
!*.d.ts
.github/
.vscode/
.env
npm-debug.log*
.DS_Store
Enter fullscreen mode Exit fullscreen mode

The Code

// src/index.ts
export interface Options {
  prefix?: string;
  separator?: string;
}

export function formatList(items: string[], options: Options = {}): string {
  const { prefix = '', separator = ', ' } = options;

  if (!items.length) return '';
  if (items.length === 1) return `${prefix}${items[0]}`;

  const last = items.pop();
  const joined = items.join(separator);
  return `${prefix}${joined} and ${last}`;
}

export function capitalize(str: string): string {
  return str.charAt(0).toUpperCase() + str.slice(1);
}
Enter fullscreen mode Exit fullscreen mode

TypeScript Config

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ES2020"],
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "bundler"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "test"]
}
Enter fullscreen mode Exit fullscreen mode

Tests (Don't Skip This!)

// test/index.test.js
const { formatList, capitalize } = require('../dist');

describe('formatList', () => {
  test('returns empty for empty array', () => {
    expect(formatList([])).toBe('');
  });

  test('returns single item', () => {
    expect(formatList(['apple'])).toBe('apple');
  });

  test('joins with separator', () => {
    expect(formatList(['apple', 'banana'])).toBe('apple and banana');
  });

  test('handles three+ items', () => {
    expect(formatList(['a', 'b', 'c'])).toBe('a, b and c');
  });

  test('respects prefix option', () => {
    expect(formatList(['x'], { prefix: '- ' })).toBe('- x');
  });
});

describe('capitalize', () => {
  test('capitalizes first letter', () => {
    expect(capitalize('hello')).toBe('Hello');
  });
});
Enter fullscreen mode Exit fullscreen mode

README.md Template

# My Awesome Package

> One-line description of what it does

[![npm version](https://img.shields.io/npm/v/my-awesome-package.svg)](https://www.npmjs.com/package/my-awesome-package)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

## Installation

\`\`\`bash
npm install my-awesome-package
# or
yarn add my-awesome-package
\`\`\`

## Usage

\`\`\`javascript
import { formatList } from 'my-awesome-package';

const result = formatList(['apple', 'banana', 'cherry']);
// 'apple, banana and cherry'
\`\`\`

## API

### \`formatList(items, options?)\`

Formats an array of strings into a readable list.

| Parameter | Type | Default |
|-----------|------|---------|
| \`items\` | \`string[]\` | Required |
| \`options.prefix\` | \`string\` | \`''\` |
| \`options.separator\` | \`string\` | \`', '\` |

## License

MIT © [Your Name](https://github.com/your-username)
Enter fullscreen mode Exit fullscreen mode

Publishing Steps

# 1. Build
npm run build

# 2. Test
npm test

# 3. Dry run (see what would be published)
npm pack --dry-run

# 4. Login (first time only)
npm login

# 5. Publish!
npm publish --access public

# For scoped packages (@username/pkg), use --access public
# For beta/pre-release: npm publish --tag next
Enter fullscreen mode Exit fullscreen mode

Versioning (Semantic Versioning)

# Patch: Bug fixes (no breaking changes)
npm version patch   # 1.0.0 → 1.0.1

# Minor: New features (backward compatible)
npm version minor   # 1.0.0 → 1.1.0

# Major: Breaking changes
npm version major   # 1.0.0 → 2.0.0

# Each command also commits + tags in git!
Enter fullscreen mode Exit fullscreen mode

After Publishing

# Your package is live at:
# https://www.npmjs.com/package/my-awesome-package

# Install it anywhere:
npm install my-awesome-package

# Update later:
npm update my-awesome-package
Enter fullscreen mode Exit fullscreen mode

Pro Tips

  1. Use scoped packages: @username/package-name avoids naming conflicts
  2. Always include TypeScript types: Makes your package usable by TS developers
  3. Support both ESM and CJS: Use exports field in package.json
  4. Write tests: Even basic tests catch regressions
  5. Use prepublishOnly: Ensures code is built before every publish
  6. Keep files minimal: Only publish what consumers need (dist/)
  7. Add CI/CD: Auto-publish on git tag push
  8. Document everything: README is your landing page on npm

Have you published an npm package? Share your experience below!

Follow @armorbreak for more developer content.

Top comments (0)