To create an npm module you initialize a package.json, write your code with a defined entry point, and publish with npm publish — but doing it securely means scoping your tokens, enabling two-factor authentication, and controlling exactly which files ship. The mechanics of creating an npm module take five minutes; the parts that protect you and everyone who installs your package take a little longer and are the parts most tutorials skip.
This guide walks the full path from empty folder to published package, with the security decisions called out where they belong.
Step 1: Initialize the Package
Create a directory and generate a package.json:
mkdir my-module && cd my-module
npm init -y
The package.json is the heart of your module. A few fields matter more than people realize:
{
"name": "my-module",
"version": "1.0.0",
"description": "A small, focused utility",
"main": "dist/index.js",
"type": "module",
"files": ["dist"],
"engines": { "node": ">=18" },
"license": "MIT"
}
The files field is a security control, not just housekeeping. It allowlists what gets published. Without it, npm publishes everything not covered by .npmignore — which is how people accidentally ship .env files, private keys, and internal notes to the public registry.
Step 2: Write the Code and Define an Entry Point
Keep the module focused. The most reused npm packages do one thing well. Point main (and module/exports for modern setups) at your built entry file:
// src/index.js
export function greet(name) {
return `Hello, ${name}`;
}
If you write in TypeScript or use modern syntax, build to a dist/ folder and publish that, keeping src/ out of the package. Ship type declarations (.d.ts) so consumers get autocomplete.
Step 3: Control What Actually Ships
Before you ever publish, check what npm would include. This is the single most important safety step:
npm pack --dry-run
This prints the exact file list of the tarball without publishing. Review it every time. If you see anything that looks like a secret, a test fixture with real data, or your entire git history, stop and fix your files allowlist or .npmignore.
Never commit or publish credentials. If you use environment variables during build, make sure .env is in both .gitignore and excluded from the package.
Step 4: Secure Your npm Account and Token
Your publishing credentials are the keys to your package's supply chain. If they leak, an attacker can publish a malicious version that every one of your users installs. Two controls are non-negotiable:
- Enable two-factor authentication on your npm account, set to require an OTP for both login and publish. This alone blocks the most common account-takeover path.
- Use granular, scoped automation tokens for CI rather than your personal login token. Scope a token to just the package it needs to publish, and store it as a masked CI secret — never in the repo.
# Interactive login with 2FA for a manual publish
npm login
# In CI, use a scoped automation token via .npmrc, injected from a secret
Step 5: Publish With Provenance
Run a final scoped-name check, then publish:
npm publish --access public --provenance
The --provenance flag, when publishing from a supported CI system like GitHub Actions, attaches a signed, verifiable statement linking the published package back to the exact source commit and build that produced it. Consumers can verify that the tarball on the registry genuinely came from your repository and wasn't tampered with. For a package other people will depend on, provenance is one of the strongest supply-chain signals you can offer for very little effort.
Use a prepublishOnly script to guarantee you never publish an unbuilt or untested package:
{
"scripts": {
"prepublishOnly": "npm run build && npm test"
}
}
Step 6: Keep the Published Package Healthy
Publishing isn't the end. A module you maintain accumulates its own dependencies, and those dependencies get CVEs over time. Scan your own dependency tree so you don't unknowingly ship a vulnerable transitive package to your users — an SCA tool can flag this in CI before a release goes out. Pin or range your dependencies deliberately, and respond to advisories promptly, because your users inherit whatever you depend on. If you're weighing publishing workflows in more depth, our companion guide on how to make an npm package covers naming, scoping, and versioning strategy.
The reputation of your package rests on the trust that installing it won't hurt someone. That trust is built from the small, boring choices above: allowlisting files, protecting tokens, publishing with provenance, and keeping dependencies patched.
FAQ
What is the minimum to create an npm module?
A directory with a valid package.json (created via npm init) and at least one code file referenced by the main field. Running npm publish from an authenticated account pushes it to the registry. Everything else — tests, TypeScript, provenance — is important for quality and safety but not strictly required to publish.
How do I stop npm from publishing secret files?
Use the files allowlist field in package.json to specify exactly which files ship, and run npm pack --dry-run before every publish to review the resulting file list. Keep .env and credential files in .gitignore. The allowlist approach is safer than a .npmignore denylist because it fails closed.
Should I enable two-factor authentication for publishing?
Yes, always. Set 2FA to require an OTP for publishing, and use scoped automation tokens for CI instead of your personal credentials. Account takeover is a primary vector for supply-chain attacks, and 2FA blocks the most common version of it.
What does npm provenance do?
Publishing with --provenance from a supported CI system attaches a signed attestation linking the package to the exact source repository, commit, and build workflow that created it. Consumers can verify this, giving them cryptographic assurance that the package wasn't tampered with between your source and the registry.