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

Publishing to npm isn't hard. But there are gotchas. Here's my experience.

The Package I Published

Name: @armorbreak/fast-safe-stringify
Purpose: Faster and safer JSON.stringify with circular reference handling
Size: 2KB minified, 0 dependencies
Time to build: 2 hours
Time to publish: 30 minutes (including learning curve)
Enter fullscreen mode Exit fullscreen mode

Step 1: Project Setup

mkdir fast-safe-stringify
cd fast-safe-stringify
npm init -y

# Essential files you need:
touch index.js      # Main code
touch README.md     # Documentation
touch .gitignore    # Ignore node_modules, etc.
touch LICENSE       # MIT license (recommended)
touch .npmignore    # What NOT to publish
Enter fullscreen mode Exit fullscreen mode

Step 2: package.json Configuration

{
  "name": "fast-safe-stringify",
  "version": "1.0.0",
  "description": "Fast, safe JSON.stringify with circular reference protection",
  "main": "index.js",
  "types": "index.d.ts",        // TypeScript declarations!
  "files": [                     // What gets published (be explicit)
    "index.js",
    "index.d.ts",
    "README.md",
    "LICENSE"
  ],
  "scripts": {
    "test": "node --test test/*.test.js",
    "prepublishOnly": "npm test", // Tests run before every publish
    "lint": "eslint index.js"
  },
  "keywords": ["json", "stringify", "circular", "fast", "safe"],
  "author": "Alex Chen <contact@agentvote.cc>",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/armorbreak001/fast-safe-stringify"
  },
  "engines": {
    "node": ">=18.0.0"           // Minimum Node.js version
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: The Code

// index.js
'use strict';

function stringify(value, replacer, space) {
  const seen = new WeakSet();

  return JSON.stringify(value, function(key, val) {
    if (typeof val === 'object' && val !== null) {
      if (seen.has(val)) return '[Circular]';
      seen.add(val);
    }

    // Handle BigInt
    if (typeof val === 'bigint') return val.toString();

    // Handle undefined in arrays
    if (typeof val === 'undefined' && Array.isArray(this)) return null;

    // Apply custom replacer
    if (replacer) {
      const result = typeof replacer === 'function'
        ? replacer(key, val)
        : replacer;
      if (result !== undefined) return result;
    }

    return val;
  }, space);
}

module.exports = stringify;
module.exports.default = stringify;
module.exports.stringify = stringify;
Enter fullscreen mode Exit fullscreen mode

Step 4: TypeScript Declarations

// index.d.ts — Even if you write in JS, provide types!
declare function stringify(
  value: any,
  replacer?: ((key: string, value: any) => any) | string[] | null,
  space?: string | number
): string;

export default stringify;
export { stringify };
Enter fullscreen mode Exit fullscreen mode

Step 5: Tests

// test/stringify.test.js
const { describe, it } = require('node:test');
const assert = require('node:assert/strict');
const stringify = require('../index.js');

describe('fast-safe-stringify', () => {
  it('stringifies basic objects', () => {
    assert.equal(stringify({ a: 1 }), '{"a":1}');
  });

  it('handles circular references', () => {
    const obj = { name: 'test' };
    obj.self = obj;
    const result = stringify(obj);
    assert.ok(result.includes('[Circular]'));
    assert.ok(!result.includes('TypeError'));
  });

  it('handles BigInt', () => {
    const result = stringify({ big: BigInt(9007199254740991) });
    assert.ok(result.includes('9007199254740991'));
  });

  it('handles undefined in arrays', () => {
    const result = stringify([1, undefined, 3]);
    assert.equal(result, '[1,null,3]');
  });

  it('supports replacer function', () => {
    const result = stringify(
      { password: 'secret', name: 'Alex' },
      (key, val) => key === 'password' ? '***' : val
    );
    assert.equal(result, '{"password":"***","name":"Alex"}');
  });

  it('supports pretty printing', () => {
    const result = stringify({ a: 1 }, null, 2);
    assert.ok(result.includes('\n'));
    assert.ok(result.includes('  '));
  });
});
Enter fullscreen mode Exit fullscreen mode

Step 6: .npmignore

# Don't publish these files:
node_modules/
test/
.github/
.git/
.eslintrc*
.prettierrc*
.vscode/
*.test.js
coverage/
.nyc_output/
Enter fullscreen mode Exit fullscreen mode

Step 7: Publish

# Check what will be published (dry run)
npm pack --dry-run

# If it looks good, publish!
npm publish

# For scoped packages (@username/package), use:
npm publish --access public

# Update version (follow semver!)
npm version patch   # 1.0.0 → 1.0.1 (bug fix)
npm version minor   # 1.0.0 → 1.1.0 (new feature, backwards compatible)
npm version major   # 1.0.0 → 2.0.0 (breaking change)

# Each npm version also creates a git tag automatically
Enter fullscreen mode Exit fullscreen mode

Things I Wish I Knew

1. Name Availability

# Check if name is available before you start!
npm view package-name

# Scoped names are always available:
@armorbreak/anything-here  ← Always available to you

# But public scoped packages need --access public flag
Enter fullscreen mode Exit fullscreen mode

2. package.json "files" Field

// Without "files": npm publishes EVERYTHING (including tests, configs, etc.)
// With "files": npm ONLY publishes what you list
{
  "files": ["index.js", "index.d.ts", "README.md", "LICENSE"]
}
// This keeps your package size small!
Enter fullscreen mode Exit fullscreen mode

3. Two-Factor Auth (REQUIRED for npm)

# npm requires 2FA for publishing. Set it up:
npm profile enable-2fa auth-and-write
# This is mandatory since 2024 — you can't publish without it
Enter fullscreen mode Exit fullscreen mode

4. README Matters

A good README = more downloads

Must include:
- Package name and one-line description
- Installation instructions
- Quick example (copy-paste ready)
- API documentation
- License badge
- Build status badge (if using CI)

Nice to have:
- Performance benchmarks
- Comparison with alternatives
- GIF showing it in action
Enter fullscreen mode Exit fullscreen mode

5. Automate with CI

# .github/workflows/publish.yml
name: Publish
on:
  push:
    tags: ['v*']    # Trigger on version tags
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          registry-url: 'https://registry.npmjs.org'
      - run: npm ci
      - run: npm test
      - run: npm publish --provenance --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

Results After 1 Month

Downloads: ~2,500
Stars: 12
Dependencies: 0
Bundle size: 2KB
No bug reports
1 feature request (custom replacer)

Cost to maintain: ~1 hour/month
Satisfaction: 💯
Enter fullscreen mode Exit fullscreen mode

Have you published an npm package? What was your experience?

Follow @armorbreak for more developer content.

Top comments (0)