A monorepo can make your Express service dependencies look like a tangled ball of yarn if you’re not careful, and this particular issue crops up when your package.json files start pointing to local packages within the monorepo, but the tooling can’t resolve them correctly during builds or runs.
Common Causes and Fixes
-
Missing
npm linkoryarn link:- Diagnosis: Run
npm list --depth=0(oryarn list --depth=0) in your service directory. If a local dependency is missing, it won’t appear. - Fix: In the root of your monorepo, navigate to the directory of the shared package (e.g.,
packages/shared-utils) and runnpm link. Then, in your service directory (e.g.,services/my-express-app), runnpm link shared-utils. - Why it works: This creates symbolic links in your
node_modulesfolders, making the local package appear as if it were installed from npm, allowing Node.js and build tools to find it.
- Diagnosis: Run
-
Incorrect
workspacesconfiguration in rootpackage.json:- Diagnosis: Check your root
package.json. Theworkspacesfield should correctly define the globs for your packages. For example:{ "name": "my-monorepo", "version": "1.0.0", "workspaces": [ "packages/*", "services/*" ], // ... } - Fix: Ensure the globs accurately reflect the directory structure where your shared packages and services reside. If
shared-utilsis inpackages/shared-utilsandmy-express-appis inservices/my-express-app, the above configuration is correct. - Why it works: This tells npm/Yarn/pnpm where to find all the packages within the monorepo, enabling hoisting and proper dependency resolution across packages.
- Diagnosis: Check your root
-
Outdated
node_modulesor inconsistent installation:- Diagnosis: Sometimes, simply running
npm installoryarn installin the root isn’t enough. Check if the local package’spackage.jsonhas"private": truewhen it shouldn’t, or vice-versa. - Fix: Delete all
node_modulesdirectories from the root and all package/service directories. Then, runnpm install(oryarn install/pnpm install) from the root of the monorepo. - Why it works: A clean install ensures that all symlinks are recreated correctly and that the dependency tree is consistent across the entire monorepo.
- Diagnosis: Sometimes, simply running
-
Build tool configuration not aware of monorepo structure:
- Diagnosis: If you’re using tools like Webpack, Rollup, or esbuild, they might not be configured to look for dependencies in the monorepo’s structure. Look for errors related to
Cannot find module 'shared-utils'during the build process. - Fix: Configure your build tool’s
resolve.modulesorresolve.pluginsto include paths pointing to your shared packages. For Webpack, you might add:
(Adjust paths based on your exact structure and build tool).// webpack.config.js module.exports = { // ... resolve: { modules: [ 'node_modules', path.resolve(__dirname, '../../node_modules'), // For hoisted deps path.resolve(__dirname, '../shared-utils/src'), // Direct path to shared lib source ], // ... }, // ... }; - Why it works: This explicitly tells the bundler where to search for modules, including those located in other packages within the monorepo.
- Diagnosis: If you’re using tools like Webpack, Rollup, or esbuild, they might not be configured to look for dependencies in the monorepo’s structure. Look for errors related to
-
mainorexportsfield in shared packagepackage.jsonis incorrect:- Diagnosis: Examine the
package.jsonof the shared package (e.g.,packages/shared-utils/package.json). Check themain,module, orexportsfields. - Fix: Ensure these fields point to the correct entry file for your shared package. For a simple CommonJS module,
main: "dist/index.js"is common. For ES modules,module: "dist/index.esm.js"might be used. If usingexports, structure it like:{ "name": "shared-utils", "version": "1.0.0", "main": "dist/index.js", "module": "dist/index.esm.js", "types": "dist/index.d.ts", "exports": { ".": { "import": "./dist/index.esm.js", "require": "./dist/index.js", "types": "./dist/index.d.ts" } }, "private": false // Important for linking } - Why it works: These fields are how Node.js and bundlers determine which file to load when you
require()orimport()the package, ensuring the correct code is picked up.
- Diagnosis: Examine the
-
Using a package manager that doesn’t support workspaces well (e.g., older npm versions):
- Diagnosis: If you’re on npm v6 or earlier, workspace support is rudimentary. Errors might be cryptic.
- Fix: Upgrade to npm v7+ or switch to Yarn v2+ or pnpm, which have robust monorepo support. If you must stay on older npm, rely heavily on
npm linkand manual installs, but this is highly discouraged. - Why it works: Newer package managers are designed with monorepos in mind, handling dependency hoisting, linking, and resolution much more effectively.
The next error you’ll likely hit after fixing these is related to environment variables not being correctly loaded or shared across services, which you’ll need to address with a shared configuration management strategy.