Skip to content

When building Node.js projects, you have two main approaches to distribute your projects final artifact from the monorepo, bundled or not bundled. Depending on your deployment strategy and tooling preferences, one approach may be more suitable than the other.

Bundled builds compile your code and all its dependencies into a single file, e.g. main.js This approach:

  • Produces a self-contained artifact that doesn't require node_modules
  • Simplifies deployment since you only need to copy a single file
  • Works well for serverless functions and containerized applications

Non-bundled builds preserve your module structure and require node_modules at runtime. This approach:

  • Requires installing dependencies before running
  • Maintains separate files for each module
  • Can improve container rebuilds speeds when paired with Docker Layer Caching

Webpack provides full control over the bundling process. Configure your webpack.config.js to bundle all dependencies.

  • generatePackageJson: false - Skips generating a package.json since all dependencies are bundled
  • externalDependencies: 'none' - Bundles all dependencies instead of treating them as external
webpack.config.js
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
const { join } = require('path');
module.exports = {
output: {
path: join(__dirname, 'dist'),
},
plugins: [
new NxAppWebpackPlugin({
target: 'node',
compiler: 'tsc',
main: './src/main.ts',
tsConfig: './tsconfig.app.json',
outputHashing: 'none',
generatePackageJson: false,
externalDependencies: 'none',
}),
],
};

To build and deploy:

Terminal window
nx build my-app
# Deploy dist/my-app/main.js - no node_modules needed

If you're publishing a library package to npm, avoid bundling dependencies. Instead, declare them in package.json so package managers can handle versioning and deduplication.

For Docker based deployments, non-bundled builds can improve build times through layer caching. When dependencies don't change, Docker reuses the cached node_modules layer, only rebuilding your application code.

Instead of running build, use the prune target which prepares your application for deployment with its dependencies:

Terminal window
nx prune my-app

This creates a deployment-ready structure:

dist/my-app/
├── main.js # Your application code
├── package.json # Dependencies manifest
└── node_modules/ # Production dependencies

In your Dockerfile, copy these files and run with Node.js:

COPY dist/my-app /app
WORKDIR /app
CMD ["node", "main.js"]

Nx recommends publishing workspace dependencies as separate packages rather than bundling them into a single library. This approach provides better versioning control and allows consumers to manage their own dependency trees.

However, bundling workspace libraries could make sense when:

  • The library contains only types that should be inlined
  • You want to distribute an internal library as part of your package

Configure the external option to control which dependencies get bundled:

project.json
{
"name": "my-lib",
"targets": {
"build": {
"executor": "@nx/esbuild:esbuild",
"options": {
"platform": "node",
"outputPath": "dist/libs/my-lib",
"format": ["cjs", "esm"],
"bundle": true,
"main": "libs/my-lib/src/index.ts",
"tsConfig": "libs/my-lib/tsconfig.lib.json",
"external": ["^[^./].*$", "!@my-org/utils"]
}
}
}
}

The external patterns work as follows:

  • "^[^./].*$" - Externalizes all npm packages (paths not starting with . or /)
  • "!@my-org/utils" - Exception: bundles @my-org/utils despite matching the first pattern

See esbuild documentation if using an esbuild configuration file directly

When building libraries that consume other workspace libraries, always define the dependency relationship in package.json:

libs/my-lib/package.json
{
"name": "@my-org/my-lib",
"dependencies": {
"@my-org/utils": "workspace:*"
}
}

The workspace:* syntax:

  • During development: resolves to the local workspace package
  • During publish: gets replaced with the actual version number

This ensures your library correctly declares its dependencies regardless of whether they're bundled.

ScenarioToolKey Settings
Bundled Node appWebpackgeneratePackageJson: false, externalDependencies: 'none'
Bundled Node appesbuildbundle: true, generatePackageJson: false
Bundled Node appViteexternal: [] in rollupOptions
Non-bundled Node appAnyRun prune target, deploy with node_modules
Publishable lib (bundle workspace deps)esbuildbundle: true, external: ["^[^./].*$", "!@org/lib"]
Publishable lib (bundle workspace deps)ViteCustom external function in rollupOptions