How to publish a NestJS module to npm

Projects creation

First you need to create two directories in the same folder. One for the library / module you want to publish and one for the tests.

  • nestjs-library
  • nestjs-library-demo

Inside the library directory, the first thing to do is to create the package.json:

npm init -y

Then you can install the dependencies:

npm install -D @nestjs/common @nestjs/core reflect-metadata rxjs @types/node rimraf typescript

Everything is installed as a dev dependency, because a NestJS module must not embed its own copy of Nest: it has to use the one of the application that installs it. To do that, the Nest packages are also declared as peer dependencies in the package.json:

"peerDependencies": {
  "@nestjs/common": ">=9.0.0",
  "@nestjs/core": ">=9.0.0",
  "reflect-metadata": ">=0.1.13",
  "rxjs": ">=7.0.0"
}

If you install them as normal dependencies instead, npm can install a second copy of @nestjs/common inside your package. The decorators of the two copies don’t recognize each other, and you get dependency injection errors that are hard to understand.

Be careful with the flag: -D is the shortcut of --save-dev. The lowercase -d is a different option (it changes the log level), and the packages end up in the dependencies.

Then inside the package.json you need to change the main, types and scripts fields with these lines

"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "rimraf dist && tsc",
    "prepublishOnly": "npm run build"
},

prepublishOnly runs the build before each publish. Don’t use prepublish for this: since npm 5 it also runs on npm install, which is not what we want here.

After that you should create a tsconfig.json (based on the nestjs app)

{
  "compilerOptions": {
    "module": "commonjs",
    "declaration": true,
    "removeComments": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "allowSyntheticDefaultImports": true,
    "target": "es2017",
    "sourceMap": true,
    "outDir": "./dist",
    "baseUrl": "./",
    "incremental": true,
    "skipLibCheck": true,
    "strictNullChecks": false,
    "noImplicitAny": false,
    "strictBindCallApply": false,
    "forceConsistentCasingInFileNames": false,
    "noFallthroughCasesInSwitch": false
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist", "**/*.spec.ts"]
}

The include and exclude are important: without them tsc compiles the tests too, and it can also try to compile the dist folder it just generated.

This file comes from the NestJS application template. For a published library, strictNullChecks and noImplicitAny are worth turning back to true: the types you generate here are the ones used by every project that installs your module.

You should create a src folder, and inside this folder you can create all your modules.

You also need to create an index.ts to export all your modules.

import { LibraryModule } from "./library.module";

export { LibraryModule }

How to publish it on npm

Preparing the package

Before publishing, some fields need to be added to the package.json of the library.

{
  "name": "my-nestjs-library",
  "version": "0.0.1",
  "description": "A short description of the module",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/<your account>/<your repository>.git"
  },
  "keywords": ["nestjs", "module"],
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "files": ["dist"]
}

The files field is the important one here. Without it npm publishes everything, including the src folder and the tests. With it, only the dist folder is sent.

If the name of your package is scoped, like @your-account/my-nestjs-library, npm considers it private by default, so you also need to allow the public access:

"publishConfig": {
  "access": "public"
}

Publishing manually

Before publishing anything, you can check what would be sent:

npm run build
npm pack --dry-run

It prints the list of the files that would be included in the package. If you see your sources or your tests in this list, the files field is missing or wrong.

Then you can log in and publish:

npm login
npm publish

For the next versions, you need to bump the version before publishing:

npm version patch
npm publish

Publishing with GitHub Actions

Doing it by hand works, but I prefer a workflow that bumps the version, updates the changelog, publishes the package and creates the GitHub release.

I did the same thing for an Angular library in Create an angular library, and the workflow below is that one adapted to a NestJS module: there is no Angular CLI to install, and the package is published from the root of the project instead of a dist/my-lib folder.

First you need a CHANGELOG.md file at the root of the project, with an unreleased section:

# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

Then you need an npm token. On npmjs.com, in the token section of your account, create an automation token (this type of token works even if you have the 2FA enabled). In your GitHub repository, in Settings -> Secrets and variables -> Actions, create a secret named NPMJS_ACCESS_TOKEN and paste the token you just created.

The workflow also commits and pushes, so it needs the write permission on the repository: in Settings -> Actions -> General, at the bottom of the page, in Workflow permissions, choose “Read and write permissions”.

Here is the workflow file, in .github/workflows/release.yml:

name: Release package
on:
  workflow_dispatch:
    inputs:
      release-type:
        description: 'Release type (one of): patch, minor, major, prepatch, preminor, premajor, prerelease'
        required: true
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      # Checkout project repository
      - name: Checkout
        uses: actions/checkout@v4

      # Setup Node.js environment
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          registry-url: https://registry.npmjs.org/
          node-version: '20'

      - name: Install dependencies
        run: npm ci

      # Configure Git
      - name: Git configuration
        run: |
          git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git config --global user.name "GitHub Actions"          

      # Bump package version
      # Use tag latest
      - name: Bump release version
        if: startsWith(github.event.inputs.release-type, 'pre') != true
        run: |
          echo "NEW_VERSION=$(npm --no-git-tag-version version $RELEASE_TYPE)" >> $GITHUB_ENV
          echo "RELEASE_TAG=latest" >> $GITHUB_ENV          
        env:
          RELEASE_TYPE: ${{ github.event.inputs.release-type }}

      # Bump package pre-release version
      # Use tag beta for pre-release versions
      - name: Bump pre-release version
        if: startsWith(github.event.inputs.release-type, 'pre')
        run: |
          echo "NEW_VERSION=$(npm --no-git-tag-version --preid=beta version $RELEASE_TYPE)" >> $GITHUB_ENV
          echo "RELEASE_TAG=beta" >> $GITHUB_ENV          
        env:
          RELEASE_TYPE: ${{ github.event.inputs.release-type }}

      - name: Build the module
        run: npm run build

      # Update changelog unreleased section with new version
      - name: Update changelog
        uses: superfaceai/release-changelog-action@v1
        with:
          path-to-changelog: CHANGELOG.md
          version: ${{ env.NEW_VERSION }}
          operation: release

      # Commit changes
      - name: Commit CHANGELOG.md and package.json changes and create tag
        run: |
          git add "package.json"
          git add "CHANGELOG.md"
          git commit -m "chore: release ${{ env.NEW_VERSION }}"
          git tag ${{ env.NEW_VERSION }}          

      # Publish version to public repository
      - name: Publish
        run: npm publish --verbose --access public --tag ${{ env.RELEASE_TAG }}
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPMJS_ACCESS_TOKEN }}

      # Push repository changes
      - name: Push changes to repository
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          git push origin && git push --tags          

      # Read version changelog
      - id: get-changelog
        name: Get version changelog
        uses: superfaceai/release-changelog-action@v1
        with:
          path-to-changelog: CHANGELOG.md
          version: ${{ env.NEW_VERSION }}
          operation: read

      # Update GitHub release with changelog
      - name: Update GitHub release documentation
        uses: softprops/action-gh-release@v1
        with:
          tag_name: ${{ env.NEW_VERSION }}
          body: ${{ steps.get-changelog.outputs.changelog }}
          prerelease: ${{ startsWith(github.event.inputs.release-type, 'pre') }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

The release-type input is the same value as the argument of npm version, so you can run the workflow with patch, minor or major, and with prepatch or prerelease to publish a beta version under the beta tag.

For the first release I use prepatch, so I can check that everything works without publishing a latest version.

The versions of the actions are the ones I used, so check the latest ones before copying this file.

Using the module in the demo project

Once the package is published, the demo project can install it like any other package:

npm install my-nestjs-library

And the module can be imported in the app.module.ts file:

import { LibraryModule } from 'my-nestjs-library';

@Module({
  imports: [LibraryModule],
})
export class AppModule {}

I used multiple tutorials and source code, here are the links.

Sources: