Creating publishable NestJS Libraries with Nx

getlarge

Edouard Maleix

Posted on April 15, 2024

Creating publishable NestJS Libraries with Nx

NestJS, a progressive Node.js framework, has been gaining popularity for composing enterprise-grade server-side applications. Its out-of-the-box support for TypeScript, combined with features like dependency injection, decorators, and modular organization, makes it a go-to choice for developers aiming for clean, scalable code architectures.

Parallelly, Nx emerges as a powerhouse toolset for managing monorepos, facilitating code sharing and reuse across projects without losing sight of boundaries. Its ability to handle complex builds and dependencies across multiple frameworks and libraries streamlines development processes, especially in large-scale projects involving NestJS alongside other technologies like Angular.

In my journey over the past four years, NestJS has proven invaluable in maintaining solid code conventions and modular codebase. The last three years have seen Nx as a pivotal element in managing applications and libraries, ensuring consistency and efficiency in development workflows.

Note
You can follow the companion repository while reading this article.

The Synergy Between NestJS and Nx

Creating libraries within a NestJS application using Nx is more than a matter of convenience; it's about embracing a pattern that enhances maintainability and scalability. This synergy allows for workflows where code reuse becomes a norm, not an afterthought. The goal is to build libraries that are not just easy to use but also easy to maintain, a challenge many developers face in the lifecycle of a software project.

Nx brings to the table a set of tools that, when combined with NestJS's architecture, promote a level of abstraction and separation of concerns that is hard to achieve otherwise.

Workspace setup

The process begins with initializing an Nx workspace configured to cater to a NestJS environment. This step sets the stage for a packaged-based monorepo structure where applications and libraries co-exist, sharing dependencies and tooling yet operating independently.

Question
What is a packaged-based monorepo?

The following command initializes an Nx workspace optimized for package management with npm, Nx also provides a preset for NestJS, which sets up the workspace with the necessary configurations for developing NestJS applications and libraries. Since we focus on libraries, we will use the @nx/nest plugin separately to generate libraries tailored for NestJS.

npx create-nx-workspace@18.1.2 your-workspace --preset=npm \
--no-interactive --nxCloud=skip --yes

cd your-workspace

# you can check the docs https://nx.dev/nx-api/nest to know more about its capabilities
npm install -D @nx/nest

Enter fullscreen mode Exit fullscreen mode

Structuring the Workspace for Library Development

Within this workspace, we create libraries tailored for specific functionalities, such as interacting with external APIs or enhancing the application's core capabilities. The structure of these libraries is crucial, as it dictates their usability and ease of maintenance.

For example, your library could encapsulate the interactions with an Identity Provider, abstracting away the complexities of direct API calls and providing a simple, intuitive interface for the rest of the application. It could also contain a guard to check users' authentication and permissions in the NestJS application when importing your library. This encapsulation follows the principles of modular design, where each module or library has a well-defined responsibility and interface.

For instance, let's create two libraries: private-nestjs-library and public-nestjs-library. The former is an internal library not meant to be published in an NPM registry, while the latter is a public library intended for external consumption.

nx g @nx/nest:lib private-nestjs-library --directory packages/private-nestjs-library \
--buildable --importPath @your-npm-scope/private-nestjs-library \
--projectNameAndRootFormat as-provided --no-interactive

nx g @nx/nest:lib public-nestjs-library --directory packages/public-nestjs-library \
--publishable --importPath @your-npm-scope/public-nestjs-library \
--projectNameAndRootFormat as-provided --no-interactive
Enter fullscreen mode Exit fullscreen mode

Note

  • The --publishable flag is used to generate a buildable and publishable library, while the --importPath flag specifies the import path for the library. This is particularly useful when publishing the library to an npm registry.
  • The --directory flag is used to specify the directory where the library will be created. This is particularly useful when organizing libraries into subdirectories within the packages directory.
  • The --projectNameAndRootFormat flag is used to ensure Nx only uses the provided project name and root format when generating the library files.

Now, let’s check the directory structure to be sure everything is correct by running tree . -d --gitignore in the terminal. You should see something like this:

├── packages
│   ├── private-nestjs-library
│   │   └── src
│   │       └── lib
│   └── public-nestjs-library
│       └── src
│           └── lib
└── tools
    └── scripts
Enter fullscreen mode Exit fullscreen mode

Which would translate to the following alias paths in tsconfig.base.json:

{
  "compilerOptions": {
    // ...
    "paths": {
      "@your-npm-scope/private-nestjs-library": [
        "packages/private-nestjs-library/src/index.ts"
      ],
      "@your-npm-scope/public-nestjs-library": [
        "packages/public-nestjs-library/src/index.ts"
      ]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

By leveraging Nx's capabilities to generate buildable and publishable libraries, we ensure that each library can be independently developed, tested, and versioned, encouraging modularization and reusability.

private-nestjs-library:

private-nestjs-library

public-nestjs-library:

public-nestjs-library

Tips
If you wonder how to visualize this project view, run the nx show project <project_name> --web

As you can see, only the public-nestjs-library contains the nx-release-publish target, which triggers the library publishing to an NPM registry. Our plan is that private-nestjs-library will be imported and shipped with the public-nestjs-library, while the public-nestjs-library will be published and used by other applications.

Library Implementation

This article focuses on the theoretical aspects of creating libraries with Nx and NestJS. In practice, it will depend on the purpose of the library and the provided functionalities; however, a typical pattern is to iterate over the following steps:

  1. Create interfaces and constants to configure the module(s)
  2. Declare a dynamic Module that the consuming application will import and take care of instantiating the providers and, eventually, controllers
  3. Create a service that will handle the business logic and expose methods to be used by the consuming application
  4. Write unit test suites for the service

When implementing your library, keep in mind best practices in software development, including but not limited to:

  • Abstraction and Encapsulation: Keeping implementation details hidden, exposing only necessary interfaces. This makes library usage and maintenance effortless.
  • Single Responsibility Principle: Each library should have one purpose and not be overloaded with functionalities that can be decoupled.
  • Dependency Injection: Facilitates testing and decouples the libraries from their dependencies.
  • Modularization: Promotes code reuse and simplifies the maintenance of large codebases.

A typical entry point of a NestJS library could look like this:

// packages/public-nestjs-library/src/lib/public-nestjs-library.module.ts
import { Module } from '@nestjs/common';

import { PrivateNestjsLibraryModule } from '@your-npm-scope/private-nestjs-library';

import { PublicNestjsLibraryOptions } from './public-nestjs-library.interfaces';
import { PublicNestjsLibraryService } from './public-nestjs-library.service';

@Module({
  imports: [PrivateNestjsLibraryModule],
  controllers: [],
  providers: [PublicNestjsLibraryService],
  exports: [PublicNestjsLibraryService],
})
export class PublicNestjsLibraryModule {
  static forRoot(
    options: PublicNestjsLibraryOptions,
    isGlobal?: boolean
  ): DynamicModule {
    return {
      module: PublicNestjsLibraryModule,
      imports: [PrivateNestjsLibraryModule],
      providers: [
        { provide: PublicNestjsLibraryOptions, useValue: options },
        PublicNestjsLibraryService,
      ],
      exports: [PublicNestjsLibraryOptions, PublicNestjsLibraryService],
      global: isGlobal,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Resulting in the following Nx dependency graph:

Project graph

Dependencies Management

One of the critical aspects of maintaining high code quality in a library-centric development environment is ensuring consistency and correctness in external dependency management. Nx addresses this with the @nx/dependency-checks lint rule, which helps keep the integrity and consistency of package.json files across the workspace.

This tool automatically checks for missing dependencies, obsolete dependencies, and version mismatches. It's instrumental in ensuring that libraries are self-contained and their dependencies are accurately reflected and up-to-date, reducing integration and compatibility issues.

For our private library private-nestjs-library, the setup is straightforward:

// packages/private-nestjs-library/.eslintrc.json
// ...
  {
    "files": [
      "*.json"
    ],
    "parser": "jsonc-eslint-parser",
    "rules": {
      "@nx/dependency-checks": [
        "error",
        {
          "buildTargets": ["build"],
          "checkMissingDependencies": true,
          "checkObsoleteDependencies": true,
          "checkVersionMismatches": true
        }
      ]
    }
  },
  // ...
Enter fullscreen mode Exit fullscreen mode

However, the public-nestjs-library, depends on private-nestjs-library that should remain internal. As a result, Nx needs to include the private-nestjs-library as part of the bundle, not as an external dependency. It should also include dependencies of the private-nestjs-library.

The solution in two parts is:

  1. In packages/public-nestjs-library/.eslintrc.json, update @nx/dependency-checks, this time using includeTransitiveDependencies and ignoredDependencies
  2. In packages/public-nestjs-library/project.json, update the targets.build.options to set "external": "none"

packages/public-nestjs-library/.eslintrc.json:

// packages/public-nestjs-library/.eslintrc.json
// ...
  {
    "files": [
      "*.json"
    ],
    "parser": "jsonc-eslint-parser",
    "rules": {
      "@nx/dependency-checks": [
        "error",
        {
          "buildTargets": ["build"],
          "checkMissingDependencies": true,
          "checkObsoleteDependencies": true,
          "checkVersionMismatches": true,
          "includeTransitiveDependencies": true,
          "ignoredDependencies": [
            "@your-npm-scope/private-nestjs-library"
          ]
        }
      ]
    }
  },
  // ...
Enter fullscreen mode Exit fullscreen mode

packages/public-nestjs-library/project.json:

{
  "targets": {
    "build": {
      "executor": "@nx/node:build",
      "options": {
        "outputPath": "dist/packages/public-nestjs-library",
        "external": "none"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, after running nx lint public-nest-library, the package.json is generated with the correct dependencies, but I suggest moving the functional dependencies to peerDependencies to avoid version conflicts with the hosting NestJS app.

If a configuration does this automatically, please share the info in the comments.

packages/public-nestjs-library/package.json:

{
  "name": "@your-npm-scope/public-nestjs-library",
  "version": "0.0.1",
  "publishConfig": {
    "access": "public"
  },
  "dependencies": {
    "tslib": "^2.3.0"
  },
  "peerDependencies": {
    "axios": "1.6.8"
  },
  "type": "commonjs",
  "main": "./src/index.js",
  "typings": "./src/index.d.ts"
}
Enter fullscreen mode Exit fullscreen mode

Tips

  • You can find some more in-depth information about @nx/dependency-checks rule in the Nx docs.
  • Remove the publishConfig field if you whish to keep the library private.

Continuous Integration

Using Nx with GitHub Actions is a powerful combination; in just few lines of code, you can end up with an efficient CI workflow that automates the testing of your libraries. And thanks to Nx, the verification only runs for the libraries affected by the current changes, saving you a lot of time and resources.

name: CI
on:
  push:
    branches:
      - main
  pull_request:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}-CI

env:
  CI: true

jobs:
  main:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci

      - uses: nrwl/nx-set-shas@v4

      - uses: 8BitJonny/gh-get-current-pr@2.2.0
        id: current-pr

      - if: steps.current-pr.outputs.number != 'null' && github.ref_name != 'main'
        # This line is needed for nx affected to work when CI is running on a PR
        run: git branch --track main origin/main

      - run: npx nx format:check

      - run: npx nx affected -t lint,test,build --parallel=3
Enter fullscreen mode Exit fullscreen mode

Tips
You can find more information about the Nx GitHub Actions and the Affected Commands in the official documentation.

Versioning and Release Management

The lifecycle of a library extends beyond its initial development. Managing versions and releases is a critical aspect of ensuring that improvements, bug fixes, and new features reach the consumers of the library in a controlled and predictable manner. With its release CLI, Nx introduces a streamlined approach to versioning and release management.

This tool simplifies bumping versions, generating changelogs, tagging releases, and publishing packages. It supports both independent and synchronized versioning strategies across the monorepo, accommodating the unique needs of each library within the workspace.

The flow is as follows:

nx release flow

I found this configuration particularly useful when using GitHub to manage releases and changelogs and NPM to publish packages. It will create independent changelogs and versions for each library.

nx.json:

// ...
  "release": {
    "projects": ["packages/*", "!packages/private-nestjs-library"],
    "projectsRelationship": "independent",
    "changelog": {
      "projectChangelogs": {
        "createRelease": "github"
      }
    },
    "version": {
      "conventionalCommits": false,
      "generatorOptions": {
        "skipLockFileUpdate": true,
        "currentVersionResolver": "git-tag"
      }
    },
    "git": {
      "commit": true,
      "tag": true
    }
  }
  "targetDefaults": {
    // ...
    "nx-release-publish": {
      "options": {
        "packageRoot": "dist/packages/{projectName}",
        "registry": "http://localhost:4873/"
      }
    }
    //...
  }
Enter fullscreen mode Exit fullscreen mode

Note

  • The projects field specifies the projects to be released, while the projectsRelationship field specifies the versioning strategy. In this case, the independent strategy is used to version each project independently and the private-nestjs-library is explicitly excluded from the release process.
  • The registry URL is set to a local Verdaccio instance, which is a private NPM registry. You can replace it with the URL of your preferred NPM registry. Verdaccio can be started with the following command: node tools/scripts/local-registry.mjs.

packages/public-nestjs-library/project.json:

{
  // ...
  "targets": {
    // ...
    "nx-release-publish": {
      "dependsOn": ["build"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Warning
In my previous experience, nx-release-publish target could be fully configured in nx.json file, but during the writing of this article dependsOn had to be set on a project basis to trigger the build target before the release.

And when it is time to release a new version, the following command gets the job done:

# for the first time you need to run the following command to create the initial tag
npx nx release --first-release

# after that, you can run the following command to release a new version
npx nx release
Enter fullscreen mode Exit fullscreen mode

Tips
You can find more information about the Nx release CLI in the official documentation.

By leveraging Nx's release management capabilities, developers can ensure their libraries align with semantic versioning principles. This process makes tracking changes and managing dependencies easier but also integrates smoothly with continuous integration pipelines, ensuring that releases are consistent, reliable, and automated.

Conclusion

The combination of NestJS and Nx offers a robust framework for creating libraries that are not just powerful but also elegant and maintainable. By embracing the theoretical principles of modular design and best practices in software development, we can build a codebase that is both scalable and easy to manage.

💖 💪 🙅 🚩
getlarge
Edouard Maleix

Posted on April 15, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related