Tools: I Published My First npm Package: Here's Everything I Wish I Knew - 2025 Update

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

Code Block

Copy

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: 💯