Tools: I Published My First npm Package: Here's Everything I Wish I Knew - 2025 Update
I Published My First npm Package: Here's Everything I Wish I Knew
The Package I Published
Step 1: Project Setup
Step 2: package.json Configuration
Step 3: The Code
Step 4: TypeScript Declarations
Step 5: Tests
Step 6: .npmignore
Step 7: Publish
Things I Wish I Knew
1. Name Availability
2. package.json "files" Field
3. Two-Factor Auth (REQUIRED for npm)
4. README Matters
5. Automate with CI
Results After 1 Month Publishing to npm isn't hard. But there are gotchas. Here's my experience. Have you published an npm package? What was your experience? Follow @armorbreak for more developer content. Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse
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)
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)
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)
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
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
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
{ "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 <[email protected]>", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/armorbreak001/fast-safe-stringify" }, "engines": { "node": ">=18.0.0" // Minimum Node.js version }
}
{ "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 <[email protected]>", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/armorbreak001/fast-safe-stringify" }, "engines": { "node": ">=18.0.0" // Minimum Node.js version }
}
{ "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 <[email protected]>", "license": "MIT", "repository": { "type": "git", "url": "https://github.com/armorbreak001/fast-safe-stringify" }, "engines": { "node": ">=18.0.0" // Minimum Node.js version }
}
// 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;
// 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;
// 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;
// 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 };
// 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 };
// 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 };
// 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(' ')); });
});
// 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(' ')); });
});
// 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(' ')); });
});
# Don't publish these files:
node_modules/
test/
.github/
.git/
.eslintrc*
.prettierrc*
.vscode/
*.test.js
coverage/
.nyc_output/
# Don't publish these files:
node_modules/
test/
.github/
.git/
.eslintrc*
.prettierrc*
.vscode/
*.test.js
coverage/
.nyc_output/
# Don't publish these files:
node_modules/
test/
.github/
.git/
.eslintrc*
.prettierrc*
.vscode/
*.test.js
coverage/
.nyc_output/
# 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
# 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
# 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
# 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
# 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
# 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
// 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!
// 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!
// 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!
# 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
# 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
# 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
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
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
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
# .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 }}
# .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 }}
# .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 }}
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: 💯
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: 💯
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: 💯