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 vs Non-Bundled Builds
Section titled “Bundled vs Non-Bundled Builds”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
Bundling Node.js Applications
Section titled “Bundling Node.js Applications”Webpack provides full control over the bundling process. Configure your webpack.config.js to bundle all dependencies.
generatePackageJson: false- Skips generating apackage.jsonsince all dependencies are bundledexternalDependencies: 'none'- Bundles all dependencies instead of treating them as external
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', }), ],};esbuild offers faster builds than Webpack. Configure bundling in your project.json:
The bundle: true option tells esbuild to include all dependencies in the output.
{ "name": "my-app", "targets": { "build": { "executor": "@nx/esbuild:esbuild", "options": { "platform": "node", "outputPath": "dist/my-app", "format": ["cjs"], "bundle": true, "main": "apps/my-app/src/main.ts", "tsConfig": "apps/my-app/tsconfig.app.json", "generatePackageJson": false, "esbuildOptions": { "outfile": "dist/my-app/main.js" } } } }}Vite can also bundle Node.js applications with vite by using the build.rollupOptions.external. Set external: [] to bundle all dependencies into the output.
import { defineConfig } from 'vite';
export default defineConfig({ build: { target: 'node18', outDir: 'dist', lib: { entry: 'src/main.ts', formats: ['cjs'], fileName: 'main', }, rollupOptions: { // Bundle all dependencies external: [], }, },});To build and deploy:
nx build my-app# Deploy dist/my-app/main.js - no node_modules neededWhen NOT to Bundle
Section titled “When NOT to Bundle”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:
nx prune my-appThis creates a deployment-ready structure:
dist/my-app/├── main.js # Your application code├── package.json # Dependencies manifest└── node_modules/ # Production dependenciesIn your Dockerfile, copy these files and run with Node.js:
COPY dist/my-app /appWORKDIR /appCMD ["node", "main.js"]Bundling Libraries
Section titled “Bundling Libraries”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:
{ "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/utilsdespite matching the first pattern
See esbuild documentation if using an esbuild configuration file directly
For Vite-based builds, configure the external function in rollupOptions:
import { defineConfig } from 'vite';
export default defineConfig({ build: { lib: { entry: 'src/index.ts', formats: ['es', 'cjs'], }, rollupOptions: { external: (id) => { // Bundle workspace libraries if (id.startsWith('@my-org/')) { return false; } // Externalize npm packages if (!id.startsWith('.') && !id.startsWith('/')) { return true; } return false; }, }, },});The external function receives each import and returns:
falseto bundle the dependencytrueto keep it as an external import
See rollupOptions from vite documentation for more information
Managing Workspace Dependencies
Section titled “Managing Workspace Dependencies”When building libraries that consume other workspace libraries, always define the dependency relationship in 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.
Quick Reference
Section titled “Quick Reference”| Scenario | Tool | Key Settings |
|---|---|---|
| Bundled Node app | Webpack | generatePackageJson: false, externalDependencies: 'none' |
| Bundled Node app | esbuild | bundle: true, generatePackageJson: false |
| Bundled Node app | Vite | external: [] in rollupOptions |
| Non-bundled Node app | Any | Run prune target, deploy with node_modules |
| Publishable lib (bundle workspace deps) | esbuild | bundle: true, external: ["^[^./].*$", "!@org/lib"] |
| Publishable lib (bundle workspace deps) | Vite | Custom external function in rollupOptions |