Publishing an npm package is straightforward, but releasing an untested package can create problems for every developer who depends on it. A missing file, incorrect export path, undeclared dependency, or broken build can make a package unusable immediately after publication.

These problems are especially important when maintaining shared libraries, command-line tools, JavaScript utilities, or TypeScript packages used by multiple applications.

npm provides several ways to test a package before making it available to other developers. You can inspect the package contents, install a local package archive, publish a package under a temporary distribution tag, and verify the installed result before promoting it to the default release tag.

This process is often called staged publishing. It helps developers validate the actual package that consumers will install instead of relying only on tests that run inside the source repository.

In this article, you will learn how npm package publishing works, how to test a package safely, how to use distribution tags, and how to prepare a reliable release workflow.

What Is npm Staged Publishing?

npm staged publishing is a release approach in which a package is prepared, tested, and made available through controlled steps before it becomes the default version for consumers.

For example, suppose you maintain a package named my-utils.

Your current stable version is 1.4.0, and you have prepared version 1.5.0 with new functionality.

Instead of immediately making the new version the default release, you can publish it with a separate distribution tag, such as beta.

The release process might look like this:

Code
Source Code
    |
    v
Run Tests and Build
    |
    v
Inspect Package Contents
    |
    v
Create Package Archive
    |
    v
Test the Archive
    |
    v
Publish Version 1.5.0 with beta Tag
    |
    v
Verify Installation and Behavior
    |
    v
Promote Version 1.5.0 to latest

This workflow separates package preparation from release promotion.

However, publishing with a temporary tag does not make the package private. If the package is published to a public npm registry, users who know its name and version may still be able to install it.

If you need to test a package without making it publicly available, test a local package archive or use a private registry.

Why Testing the Published Package Matters

A package can pass its unit tests and still fail when installed in another project.

This happens because tests often run against source files in the repository. Consumers, however, install the files included in the published package.

Consider a TypeScript package with the following structure:

Code
my-utils/
├── src/
│   └── index.ts
├── test/
│   └── index.test.ts
├── package.json
├── tsconfig.json
└── README.md

Your source code might work correctly, but the package could fail for several reasons:

  • The compiled JavaScript files are missing.

  • The exports field points to the wrong location.

  • TypeScript declaration files are not included.

  • A runtime dependency is missing from dependencies.

  • Required files are excluded by .npmignore or the files field.

  • The package works with one module system but fails with another.

  • The published archive contains unnecessary development files.

Testing the actual package archive helps identify these problems before they reach consumers.

Step 1: Prepare the Package for Release

Start by reviewing the package configuration.

A TypeScript package might use the following package.json structure:

JSON
{
  "name": "my-utils",
  "version": "1.5.0",
  "description": "Reusable JavaScript utility functions",
  "main": "./dist/index.cjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  },
  "files": [
    "dist",
    "README.md",
    "LICENSE"
  ],
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "test": "node --test",
    "prepack": "npm run build"
  }
}

This configuration is illustrative. The build process must actually generate the files referenced by main, types, and exports.

In particular, the example assumes that the build produces both an ES module file and a CommonJS file. A standard TypeScript compilation does not automatically generate both outputs from every configuration.

Configure the build system to generate the required formats before using this manifest.

Understanding the Important Fields

The name and version fields identify the package and its release version.

The main field identifies the CommonJS entry point for tools that use that convention. The types field identifies the TypeScript declaration file.

The exports field defines the package entry points that consumers can access. Incorrect export paths are a common cause of installation and import failures.

The files field specifies which files should be included in the published package. This helps prevent test files, development configuration, and other unnecessary files from being distributed.

The prepack script runs before npm creates the package archive. This is useful when the compiled output must exist before packaging.

Before publishing, verify that your build configuration produces every file referenced by the package manifest.

Step 2: Run Tests and Build the Package

Run the package's test suite and build process:

Bash
npm ci
npm test
npm run build

The npm ci command installs dependencies from the lockfile and is useful for repeatable CI builds.

The test command should verify the package's expected behavior, including important edge cases.

The build command should generate all runtime and type declaration files required by consumers.

You should also run static checks, linting, or type checking when these are part of your project's quality requirements.

Do not publish a package merely because the build succeeds. A successful build confirms that the configured compilation completed; it does not prove that the resulting package works in a consumer application.

Step 3: Inspect the Package Contents

Before publishing, inspect the files npm will include in the package.

Run:

Bash
npm pack --dry-run

This command previews the package contents without creating the final archive.

Review the output carefully.

For a TypeScript package, you should normally see the compiled JavaScript files, declaration files, package manifest, documentation, and license file when applicable.

Check that the archive does not contain unnecessary files such as local environment files, private credentials, large development datasets, or internal configuration.

You can also generate the actual package archive:

Bash
npm pack

npm creates a compressed package archive, typically with a name similar to:

Code
my-utils-1.5.0.tgz

This archive represents the package artifact that can be installed by another project.

Testing this archive is more reliable than testing only the source directory because it validates the files included in the distributable package.

Step 4: Install the Package in a Separate Project

Create a temporary consumer project outside the package repository.

Bash
mkdir ../my-utils-consumer
cd ../my-utils-consumer

npm init -y

Install the package archive generated in the previous step:

Bash
npm install ../my-utils/my-utils-1.5.0.tgz

Adjust the relative path to match your directory structure.

Now create a small test file that imports and uses the package.

For example, suppose the package exports an add function:

JavaScript
import { add } from "my-utils";

console.log(add(10, 20));

The expected output is:

Code
30

Run the test using a compatible Node.js configuration.

If your package supports both CommonJS and ES modules, test both import styles in separate consumer scenarios.

Also test TypeScript compilation when the package advertises TypeScript support.

For example, create a TypeScript file that imports the package and uses its public API. Compile it using the TypeScript version and module-resolution settings that your consumers are expected to use.

This can reveal missing declaration files, incorrect exports, and incompatible type definitions.

Step 5: Publish the Package Under a Temporary Tag

After testing the archive locally, you can publish the package to npm under a temporary distribution tag.

First, make sure you are authenticated to the correct registry and have permission to publish the package.

Then run:

Bash
npm publish --tag beta

This command publishes the package version using the beta distribution tag instead of the default latest tag.

Consumers can install the staged version explicitly:

Bash
npm install my-utils@beta

Alternatively, they can install an exact version:

Bash
npm install [email protected]

Using an exact version is particularly useful in automated tests because it identifies the precise release being validated.

Distribution tags are pointers to published versions. They do not create a separate package or isolate the release from the registry.

For public packages, remember that publishing makes the package available according to the registry's access and visibility rules. Do not publish confidential code or secrets as part of a staging test.

Step 6: Verify the Published Version

After publishing the beta version, install it in a clean consumer environment.

Bash
npm view my-utils dist-tags

This command displays the package's distribution tags.

To inspect the published version:

Bash
npm view my-utils@beta version

Then install the beta package in a separate project:

Bash
npm install my-utils@beta

Run the package's integration tests against the installed version.

Verify that the package can be imported, that its public methods behave as expected, and that required dependencies are installed correctly.

If your package includes TypeScript declarations, verify type checking as well.

You should also check the package's documentation and usage examples against the actual published API.

Step 7: Promote the Tested Version to Stable

Once the staged version passes the required checks, you can move the default distribution tag to the approved version.

For example:

Bash
npm dist-tag add [email protected] latest

This changes the latest tag to point to version 1.5.0.

Consumers who install the package without specifying a version or tag will generally receive the version selected by the registry's default tag behavior.

Promoting a distribution tag does not rebuild or republish the package. It changes which already-published version the tag identifies.

Keep the beta tag if you still need it, or update it according to your release policy.

Before promoting the release, confirm that the exact version being promoted is the one that passed testing.

Staged Publishing Compared With Other Testing Methods

Different testing approaches serve different purposes.

Method

What it validates

Does it publish to npm?

Unit tests

Individual functions and logic

No

Build verification

Compilation and generated files

No

npm pack --dry-run

Expected package contents

No

Local .tgz installation

Packaging and consumer installation

No

Temporary distribution tag

Published package availability and registry installation

Yes

Private registry

Package behavior in a controlled registry environment

Yes, to that registry

Stable release promotion

Default distribution tag selection

No new package publication

A reliable release process combines several of these methods rather than relying on a single check.

Local archive testing is especially valuable because it validates the package without requiring an initial public release.

Best Practices for Safe npm Releases

Automate the Release Checks

Run tests, builds, package-content inspection, and consumer installation checks in continuous integration.

Automated checks reduce the chance that a release depends on a developer remembering every manual step.

For critical packages, test the packed archive in a separate directory before allowing the release workflow to continue.

Use Immutable Version Numbers

Treat published package versions as immutable release artifacts.

If a version contains a defect, publish a new version rather than attempting to replace the existing release with different contents.

This helps ensure that consumers installing the same version receive a consistent artifact.

Keep Dependencies Accurate

Runtime dependencies belong in dependencies when the published package needs them at runtime and expects npm to install them for consumers.

Packages required only for development and testing generally belong in devDependencies.

Peer dependencies should be declared when the package expects the consuming application to provide a compatible dependency.

An incorrect dependency declaration can cause a package to work inside its repository but fail after installation.

Protect Publishing Credentials

Use appropriately scoped credentials and follow npm's supported authentication and publishing-security practices.

In CI, use protected secrets or trusted publishing mechanisms where supported.

Do not store access tokens in source files or include them in package archives.

Test Backward Compatibility

For libraries used by other applications, check whether the release changes public methods, exported types, configuration options, or runtime behavior.

A package may pass its own tests but still break existing consumers.

Maintain tests for important public APIs and verify breaking changes against the package's versioning policy.

Common Mistakes and Troubleshooting

Problem

Possible cause

Recommended solution

Package imports fail

Incorrect exports, main, or module configuration

Inspect the manifest and test imports in a clean project

TypeScript cannot find declarations

Missing declaration files or incorrect types path

Verify the build output and package contents

Required files are missing

Incorrect files or .npmignore configuration

Review npm pack --dry-run output

Installation fails because a dependency is missing

Incorrect dependency declaration

Move required runtime dependencies into the appropriate field

Beta installation returns an unexpected version

Distribution tag points to another release

Inspect tags and install the exact version when testing

Stable users receive the wrong version

The latest tag was not updated correctly

Verify the tag after promotion

Publishing is rejected

Authentication, package permissions, or version conflict

Check registry access and use a new version when required

Advantages and Disadvantages of Staged Publishing

Staged publishing helps developers verify a package before making it the default release. It reduces the risk of broken exports, missing files, and incorrect dependencies reaching consumers.

Distribution tags also make it easier to share a release with selected testers while keeping the default tag unchanged.

However, a temporary tag does not make a public package private. The package is still published to the registry, and testers must know which tag or version to install.

Staged publishing also introduces additional release steps. Teams need a clear promotion process and should ensure that the version promoted to stable is the exact artifact that was tested.

Summary

npm staged publishing provides a controlled way to validate packages before promoting them to a stable release.

Start by running tests and building the package. Inspect the archive with npm pack --dry-run, install the generated .tgz file in a separate consumer project, and verify imports, dependencies, and TypeScript declarations.

When registry-based testing is required, publish the package with a temporary distribution tag such as beta. After the required checks pass, promote the tested version to latest.

The most important principle is to test the package consumers will actually install, not just the source code in your repository. This makes npm releases more predictable and reduces avoidable problems for downstream developers.