DEV Community

Alex Chen
Alex Chen

Posted on

How to Publish Your First npm Package (Step by Step)

How to Publish Your First npm Package (Step by Step)

Your code could be someone else's dependency. Here's how.

Step 1: Set Up the Project

mkdir my-awesome-package
cd my-awesome-package

npm init -y
# Creates package.json with defaults
Enter fullscreen mode Exit fullscreen mode
{
  "name": "@armorbreak/my-awesome-package",
  "version": "1.0.0",
  "description": "A brief description of what it does",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "type": "module",              // ES modules!
  "scripts": {
    "build": "tsc",
    "test": "vitest",
    "lint": "eslint src/",
    "prepublishOnly": "npm run build && npm test"
  },
  "keywords": ["utility", "javascript"],
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/yourname/my-awesome-package.git"
  },
  "files": [                    // What gets published!
    "dist",
    "README.md"
  ]
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Write 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 (!Array.isArray(items)) {
    throw new TypeError('Expected an array');
  }

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

  const allButLast = items.slice(0, -1).join(separator);
  const last = items[items.length - 1];

  return `${prefix}${allButLast} and ${last}`;
}

// Named export for tree-shaking
export { formatList as default };
Enter fullscreen mode Exit fullscreen mode

Step 3: Add Tests

// src/index.test.ts
import { describe, it, expect } from 'vitest';
import { formatList } from './index';

describe('formatList', () => {
  it('formats empty array', () => {
    expect(formatList([])).toBe('');
  });

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

  it('formats two items', () => {
    expect(formatList(['apple', 'banana'])).toBe('apple and banana');
  });

  it('formats multiple items with custom separator', () => {
    expect(formatList(['a', 'b', 'c'], { separator: '; '}))
      .toBe('a; b and c');
  });

  it('adds prefix when specified', () => {
    expect(formatList(['x', 'y'], { prefix: 'Items: ' }))
      .toBe('Items: x and y');
  });

  it('throws on non-array input', () => {
    expect(() => formatList(null as any)).toThrow(TypeError);
  });
});
Enter fullscreen mode Exit fullscreen mode

Step 4: Build Configuration

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

Step 5: Documentation

<!-- README.md -->
# My Awesome Package 🚀

A brief description of what your package does.

## Installation

\`\`\`bash
npm install @armorbreak/my-awesome-package
\`\`\`

## Usage

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

formatList(['apple', 'banana', 'cherry']);
// → "apple, banana and cherry"

formatList(['x', 'y'], { prefix: 'I like ', separator: ' & ' });
// → "I like x & y"
\`\`\`

## API

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

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| \`items\` | \`string[]\` | required | Array of strings |
| \`options.prefix\` | \`string\` | \`''\` | Text before the list |
| \`options.separator\` | \`string\` | \`', '\` | Between items |

**Returns:** \`string\`

## License

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

Step 6: Prepare for Publishing

# Login to npm (one-time)
npm login

# Build
npm run build

# Test locally first
npm link          # Create global symlink
# In another project:
npm link @armorbreak/my-awesome-package  # Use local version

# Dry run — see what would be published without actually publishing
npm publish --dry run
Enter fullscreen mode Exit fullscreen mode

Step 7: Publish!

# First version
npm publish --access public   # Required for scoped packages (@scope/name)

# Patch update (bug fix)
npm version patch             # 1.0.0 → 1.0.1
npm publish                  # Auto-incremented

# Minor update (new feature)
npm version minor             # 1.0.1 → 1.1.0
npm publish

# Major update (breaking change)
npm version major             # 1.1.0 → 2.0.0
npm publish
Enter fullscreen mode Exit fullscreen mode

Pro Tips

# Use .npmignore to exclude files from publishing
node_modules/
test/
*.ts
!.d.ts
.github/
.vscode/

# Two-factor auth (recommended!)
npm profile enable-2fa

# Automated releases with GitHub Actions
# .github/workflows/publish.yml triggers on version tag push

# Check if a name is taken
npm view package-name         # Shows info if exists, 404 if available
Enter fullscreen mode Exit fullscreen mode

Versioning Cheat Sheet

1.0.0
│ │ └── Patch: Bug fixes (no API changes)
│ └──── Minor: New features (backward compatible)
└────── Major: Breaking changes

Examples:
1.0.0 → 1.0.1 (Fixed typo in README)
1.0.1 → 1.1.0 (Added new option)
1.1.0 → 2.0.0 (Changed function signature)
Enter fullscreen mode Exit fullscreen mode

Have you published an npm package? Share the link below!

Follow @armorbreak for more developer content.

Top comments (0)