---
title: Node.js handbook
toc_max_heading_level: 6
---

import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

Utilizing JavaScript (and TypeScript) in a monorepo can be a daunting task, especially when using
Node.js, as there are many ways to structure your code and to configure your tools. With this
handbook, we'll help guide you through this process.

:::info

This guide is a living document and will continue to be updated over time!

:::

## moon setup

For this part of the handbook, we'll be focusing on [moon](/moon), our task runner. To start,
languages in moon act like plugins, where their functionality and support _is not_ enabled unless
explicitly configured. We follow this approach to avoid unnecessary overhead.

### Enabling the language

To enable JavaScript support via Node.js, define the [`node`](../../config/toolchain#node) setting
in [`.moon/toolchains.*`](../../config/toolchain), even if an empty object. The
[`javascript`](../../config/toolchain#javascript) toolchain must also be enabled, and configured to
use Bun as the package manager.

```yaml title=".moon/toolchains.yml"
# Enable JavaScript
javascript:
  packageManager: 'pnpm'

# Enable Node.js and pnpm
node: {}
pnpm: {}
```

Or by pinning a `node` version in [`.prototools`](../../proto/config) in the workspace root.

```toml title=".prototools"
node = "24.0.0"
pnpm = "7.29.0"
```

This will enable the Node.js toolchain and provide the following automations around its ecosystem:

- Node modules will automatically be installed if dependencies in `package.json` have changed, or
  the lockfile has changed, since the last time a task has ran.
  - We'll also take `package.json` workspaces into account and install modules in the correct
    location; either the workspace root, in a project, or both.
- Relationships between projects will automatically be discovered based on `dependencies`,
  `devDependencies`, and `peerDependencies` in `package.json`.
  - The versions of these packages will also be automatically synced when changed.
- Tasks can be [automatically inferred](../../config/toolchain#infertasksfromscripts) from
  `package.json` scripts.
- And much more!

### Utilizing the toolchain

When a language is enabled, moon by default will assume that the language's binary is available
within the current environment (typically on `PATH`). This has the downside of requiring all
developers and machines to manually install the correct version of the language, _and to stay in
sync_.

Instead, you can utilize [moon's toolchain](../../concepts/toolchain), which will download and
install the language in the background, and ensure every task is executed using the exact version
across all machines.

Enabling the toolchain is as simple as defining the [`node.version`](../../config/toolchain#version)
setting.

```yaml title=".moon/toolchains.yml"
# Enable Node.js toolchain with an explicit version
node:
  version: '18.0.0'
```

> Versions can also be defined with [`.prototools`](../../proto/config).

### Using `package.json` scripts

If you're looking to prototype moon, or reduce the migration effort to moon tasks, you can configure
moon to inherit `package.json` scripts, and internally convert them to moon tasks. This can be
achieved with the [`javascript.inferTasksFromScripts`](../../config/toolchain#infertasksfromscripts)
setting.

```yaml title=".moon/toolchains.yml"
javascript:
  inferTasksFromScripts: true
```

Or you can run scripts through `npm run` (or `pnpm`, `yarn`) calls.

```yaml title="moon.yml"
tasks:
  build:
    command: 'npm run build'
```

## Repository structure

JavaScript monorepo's work best when projects are split into applications and packages, with each
project containing its own `package.json` and dependencies. A root `package.json` must also exist
that pieces all projects together through workspaces.

For small repositories, the following structure typically works well:

```
/
├── .moon/
├── package.json
├── apps/
│   ├── client/
|   |   ├── ...
│   |   └── package.json
│   └── server/
|       ├── ...
│       └── package.json
└── packages/
    ├── components/
    |   ├── ...
    │   └── package.json
    ├── theme/
    |   ├── ...
    │   └── package.json
    └── utils/
        ├── ...
        └── package.json
```

For large repositories, grouping projects by team or department helps with ownership and
organization. With this structure, applications and libraries can be nested at any depth.

```
/
├── .moon/
├── package.json
├── infra/
│   └── ...
├── internal/
│   └── ...
├── payments/
│   └── ...
└── shared/
    └── ...
```

### Applications

Applications are runnable or executable, like an HTTP server, and are pieced together with packages
and its own encapsulated code. They represent the whole, while packages are the pieces. Applications
can import and depend on packages, but they _must not_ import and depend on other applications.

In moon, you can denote a project as an application using the [`layer`](../../config/project#layer)
setting in [`moon.*`](../../config/project).

```yaml title="moon.yml"
layer: 'application'
```

### Packages

Packages (also known as a libraries) are self-contained reusable pieces of code, and are the
suggested pattern for [code sharing](#code-sharing). Packages can import and depend on other
packages, but they _must not_ import and depend on applications!

In moon, you can denote a project as a library using the [`layer`](../../config/project#layer)
setting in [`moon.*`](../../config/project).

```yaml title="moon.yml"
layer: 'library'
```

### Configuration

Every tool that you'll utilize in a repository will have its own configuration file. This will be a
lot of config files, but regardless of what tool it is, where the config file should go will fall
into 1 of these categories:

- **Settings are inherited by all projects.** These are known as universal tools, and enforce code
  consistency and quality across the entire repository. Their config file must exist in the
  repository root, but may support overrides in each project.
  - Examples: Babel, [ESLint](../examples/eslint), [Prettier](../examples/prettier),
    [TypeScript](../examples/typescript)
- **Settings are unique per project.** These are developers tools that must be configured separately
  for each project, as they'll have different concerns. Their config file must exist in each
  project, but a shared configuration may exist as a base (for example, Jest presets).
  - Examples: [Jest](../examples/jest), [TypeScript](../examples/typescript) (with project
    references)
- **Settings are one-offs.** These are typically for applications or tools that require their own
  config, but aren't prevalent throughout the entire repository.
  - Examples: [Astro](../examples/astro), [Next](../examples/next), [Nuxt](../examples/nuxt),
    [Remix](../examples/remix), Tailwind

## Dependency management

Dependencies, also known as node modules, are required by all projects, and are installed through a
package manager like npm, pnpm, or yarn. It doesn't matter which package manager you choose, but we
highly suggest choosing one that has proper workspaces support. If you're unfamiliar with
workspaces, they will:

- Resolve all `package.json`'s in a repository using glob patterns.
- Install dependencies from all `package.json`'s at once, in the required locations.
- Create symlinks of local packages in `node_modules` (to emulate an installed package).
- Deduplicate and hoist `node_modules` when applicable.

All of this functionality enables robust monorepo support, and can be enabled with the following:

import PackageWorkspaces from '../../__partials__/node/package-workspaces.mdx';

<PackageWorkspaces />

:::info

Package workspaces are not a requirement for monorepos, but they do solve an array of problems
around module resolution, avoiding duplicate packages in bundles, and general interoperability.
Proceed with caution for non-workspaces setups!

:::

### Workspace commands

The following common commands can be used for adding, removing, or managing dependencies in a
workspace. View the package manager's official documentation for a thorough list of commands.

import WsCommands from './__partials__/workspace-commands.mdx';

<WsCommands />

### Developer tools at the root

While not a strict guideline to follow, we've found that installing universal developer tool related
dependencies (Babel, ESLint, Jest, TypeScript, etc) in the root `package.json` as `devDependencies`
to be a good pattern for consistency, quality, and the health of the repository. It provides the
following benefits:

- It ensures all projects are utilizing the same version (and sometimes configuration) of a tool.
- It allows the tool to easily be upgraded. Upgrade once, applied everywhere.
- It avoids conflicting or outdated versions of the same package.

With that being said, this _does not_ include development dependencies that are unique to a project!

### Product libraries in a project

Product, application, and or framework specific packages should be installed as production
`dependencies` in a project's `package.json`. We've found this pattern to work well for the
following reasons:

- Application dependencies are pinned per project, avoiding accidental regressions.
- Applications can upgrade their dependencies and avoid breaking neighbor applications.

## Code sharing

One of the primary reasons to use a monorepo is to easily share code between projects. When code is
co-located within the same repository, it avoids the overhead of the "build -> version -> publish to
registry -> upgrade in consumer" workflow (when the code is located in an external repository).

Co-locating code also provides the benefit of fast iteration, fast adoption, and easier migration
(when making breaking changes for example).

With [package workspaces](#dependency-management), code sharing is a breeze. As mentioned above,
every project that contains a `package.json` that is part of the workspace, will be symlinked into
`node_modules`. Because of this, these packages can easily be imported using their `package.json`
name.

```ts
// Imports from /packages/utils/package.json
import utils from '@company/utils';
```

### Depending on packages

Because packages are symlinked into `node_modules`, we can depend on them as if they were normal npm
packages, but with 1 key difference. Since these packages aren't published, they do not have a
version to reference, and instead, we can use the special `workspace:^` version (yarn and pnpm only,
use `*` for npm).

```json
{
  "name": "@company/consumer",
  "dependencies": {
    "@company/provider": "workspace:^"
  }
}
```

The `workspace:` version basically means "use the package found in the current workspace". The `:^`
determines the version range to _substitute with when publishing_. For example, the `workspace:^`
above would be replaced with version of `@company/provider` as `^<version>` when the
`@company/consumer` package is published.

There's also `workspace:~` and `workspace:*` which substitutes to `~<version>` and `<version>`
respectively. We suggest using `:^` so that version ranges can be deduped.

### Types of packages

When sharing packages in a monorepo, there's typically 3 different kinds of packages:

#### Local only

A local only package is just that, it's only available locally to the repository and _is not_
published to a registry, and _is not_ available to external repositories. For teams and companies
that utilize a single repository, this will be the most common type of package.

A benefit of local packages is that they do not require a build step, as source files can be
imported directly ([when configured correctly](#bundler-integration)). This avoids a lot of
`package.json` overhead, especially in regards to `exports`, `imports`, and other import patterns.

#### Internally published

An internal package is published to a private registry, and _is not_ available to the public.
Published packages are far more strict than local packages, as the `package.json` structure plays a
much larger role for downstream consumers, as it dictates how files are imported, where they can be
found, what type of formats are supported (CJS, ESM), so on and so forth.

Published packages require a build step, for both source code and TypeScript types (when
applicable). We suggest using [esbuild](https://esbuild.github.io/) or
[Packemon](../examples/packemon) to handle this entire flow. With that being said, local projects
can still [import their source files](#bundler-integration).

#### Externally published

An external package is structured similarly to an internal package, but instead of publishing to a
private registry, it's published to the npm public registry.

External packages are primarily for open source projects, and require the repository to also be
public.

### Bundler integration

Co-locating packages is great, but how do you import and use them effectively? The easiest solution
is to configure resolver aliases within your bundler (Webpack, Vite, etc). By doing so, you enable
the following functionality:

- Avoids having to build (and rebuild) the package everytime its code changes.
- Enables file system watching of the package, not just the application.
- Allows for hot module reloading (HMR) to work.
- Package code is transpiled and bundled alongside application code.

<Tabs
  groupId="bundler"
  defaultValue="vite"
  values={[
    { label: 'Vite', value: 'vite' },
    { label: 'Webpack', value: 'webpack' },
  ]}
>
<TabItem value="vite">

```ts title="vite.config.ts"
import path from 'path';
import { defineConfig } from 'vite';

export default defineConfig({
  // ...
  resolve: {
    alias: {
      '@company/utils': path.join(__dirname, '../packages/utils/src'),
    },
  },
});
```

</TabItem>
<TabItem value="webpack">

```ts title="webpack.config.js"
const path = require('path');

module.exports = {
  // ...
  resolve: {
    alias: {
      '@company/utils': path.join(__dirname, '../packages/utils/src'),
    },
  },
};
```

</TabItem>
</Tabs>

:::info

When configuring aliases, we suggest using the `package.json` name as the alias! This ensures that
on the consuming side, you're using the package as if it's a normal node module, and avoids
deviating from the ecosystem.

:::

### TypeScript integration

We suggest using TypeScript project references. Luckily, we have an
[in-depth guide on how to properly and efficiently integrate them](./typescript-project-refs)!
