Publishing Your Own npm Package
Take a small library from a local file to a versioned, installable package the whole ecosystem can use
npm install. Now you flip the arrow and become a producer. Publishing is not magic: a package is just a folder with a well-formed package.json manifest and a tarball uploaded to a registry. Get the manifest right and the rest is three commands.
- write a publish-ready
package.json--name,version,exports,files,license,engines; - choose the correct semantic version bump for any change;
- expose entry points with the modern
exportsmap, including dual ESM/CJS builds; - control exactly which files ship using
filesvs.npmignore; - publish a scoped package publicly and manage the
deprecate/dist-tag lifecycle.
1. package.json -- the manifest that defines your package
A package is identified to the registry by exactly two fields: its name and its version. Together they form an immutable coordinate -- @acme/slugify@1.0.0 means one specific tarball that can never be changed once published. Everything else in the manifest describes how that tarball is resolved and installed.
{
"name": "@acme/slugify",
"version": "1.0.0",
"description": "Turn any string into a clean URL slug",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"files": ["dist", "README.md"],
"keywords": ["slug", "slugify", "url", "seo"],
"license": "MIT",
"engines": { "node": ">=18" },
"repository": { "type": "git", "url": "git+https://github.com/acme/slugify.git" },
"publishConfig": { "access": "public" }
}
The fields that matter most at publish time:
name-- globally unique on the registry. Lowercase, URL-safe; may be scoped like@acme/slugify.version-- a semver string; must be higher than any version already published under that name.exports-- the modern entry-point map (supersedesmain). It declares precisely what consumers may import.files-- an allow-list of what goes into the tarball. Without it, npm ships nearly everything in the folder.license-- an SPDX identifier such asMIT; omitting it makes your package legally unusable by many teams.engines-- the Node versions you support, e.g."node": ">=18"; a warning, not a hard block, for consumers.
Dependencies split three ways. dependencies are installed automatically for your consumers. devDependencies (test runners, bundlers, linters) are not -- they exist only in your own repo. peerDependencies declare a package you expect the host app to already provide (a plugin needing its framework), and the consumer installs it, not you.