Node.js TypeScript monorepo via NPM workspaces
Tags: javascript, typescript, npm, monorepo
Published: 2022-08-14 21:01:59

---
title: Node.js TypeScript monorepo via NPM workspaces
published: true
description: Monorepos are all the rage right now. Build a Node.js TypeScript monorepo without all the complex tools - using only NPM workspaces.
tags: JavaScript,TypeScript,npm,monorepo
canonical_url: https://daveiscoding.com/nodejs-typescript-monorepo-via-npm-workspaces
---

Monorepos are all the rage right now. Modern projects are all using [NX](https://nx.dev/) to set up a monorepo. But why would you introduce such a complex tool into your tech stack when something simple is often enough?

Both Yarn and NPM include workspace management in their feature-set. Thus you can manage multiple projects in one workspace. In addition, one of the tools is always available in your project, so why not use it?

## The fantastic project

You are working on a fantastic project that you happened to name `fantastic`. How creative, isn't it?

`fantastic` is a command line application that will showcase how to set up a TypeScript monorepo using NPM workspaces. The `fantastic` project was a massive success as a CLI application, and many users wanted to have a graphical user interface to use it. So you decided to create a web interface. Your code currently lives in a single module containing the core logic and the CLI entry-point.

Therefore you decided to separate the project into three separate packages:
- `core` - this package contains the core logic of your `fantastic` project
- `web` - provides a web interface that interacts with the `core` package
- `cli` - provides a command line interface that interacts with the `core` package

### Initialize the project

Let's create an empty directory and initialize an NPM package:

```bash
mkdir fantastic
cd fantastic
npm init -y
```

Now create the packages:

```bash
npm init -y --scope @fantastic -w packages/core
npm init -y --scope @fantastic -w packages/web
npm init -y --scope @fantastic -w packages/cli
```

Define the dependencies between the packages:
```bash
npm install @fantastic/core -w @fantastic/web
npm install @fantastic/core -w @fantastic/cli
```

### Test it!

Now that we have the foundation in place, let's add some code to test it:

**packages/core/index.js**
```js
console.log("Hello from Core!");
```

**packages/web/index.js**
```js
require("@fantastic/core");
console.log("Hello from Web!");
```

**packages/cli/index.js**
```js
require("@fantastic/core");
console.log("Hello from CLI!");
```

Running the CLI outputs the following:

```bash
node packages/cli/index.js
```

```text
Hello from Core!
Hello from CLI!
```

This confirms that the setup is working fine.

### Here comes TypeScript

Time to turn this project from JavaScript to TypeScript!

First of all, install `typescript` as a dev dependency in the workspace project:

```bash
npm install -D typescript
```

Every package requires its own `tsconfig.json` file. Since the `fantastic` project uses the same configuration for all the three packages, create a common `tsconfig.base.json` file in the root directory.

**tsconfig.base.json**
```json
{
  "compilerOptions": {
    "incremental": true,
    "target": "es2020",
    "module": "commonjs",
    "declaration": true,
    "sourceMap": true,
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,

    "composite": true
  }
}
```

This is just a typical `tsconfig.json` file, except for one compiler option: `composite`. This option makes it possible for TypeScript to determine if a project has been built yet quickly.

Now you have a common TS config file, but you aren't using it yet. Create a `tsconfig.json` file in each package's root directory:

**packages/core/tsconfig.json**
```json
{
  "extends": "../../tsconfig.base.json"
}
```

The `cli` and `web` package is a bit different. You need to list out all your dependencies in the `references` property:

**packages/cli/tsconfig.json** and **packages/web/tsconfig.json**
```json
{
  "extends": "../../tsconfig.base.json",
  "references": [{ "path": "../core" }]
}
```

Now that independent packages are setup, create the `tsconfig.json` in the root directory:

**tsconfig.json**
```json
{
  "files": [],
  "references": [
    { "path": "packages/core" },
    { "path": "packages/cli" },
    { "path": "packages/web" }
  ]
}
```

Empty `files` array tells TypeScript to ignore all files except those in the references.

Rename all your `.js` files to `.ts` and replace `require` with `import`:

```js
require("@fantastic/core");
```

to

```ts
import "@fantastic/core";
```

You are ready to compile:

```bash
npx tsc --build
```

`--build` flag is required because the project contains multiple projects.

Now that you are done with all these changes, test your app again:

```bash
$ node packages/cli/index.js
Hello from Core!
Hello from CLI!

$ node packages/web/index.js
Hello from Core!
Hello from Web!
```

### Separate source code and build output

First of all, remove all the build outputs from the previous step. The following command will delete all `.js`, `.js.map`, `.d.ts` and `.tsbuildinfo` files in the `packages` directory.

```bash
rm packages/**/{*.js,*.js.map,*.d.ts,*.tsbuildinfo}
```

Having your source code and build output in different directories is a good practice. Therefore, move each package's source code into an `src` directory, and change the build output directory to `dist`.

Extend your `packages/*/tsconfig.json` files with the following snippet:

```json
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "dist"
  },
```

As an example, this is how the `packages/web/tsconfig.json` looks now:

```json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "dist"
  },
  "references": [{ "path": "../core" }]
}
```

This tells TypeScript that your source code files are in the `src` directory, and the build output should go into the `dist` directory. These are relative to your `tsconfig.json` file.

Move your `index.ts` files into the respective `src` directory. At this point, you should have the following directory tree:

```
├── package-lock.json
├── package.json
├── packages
│   ├── cli
│   │   ├── package.json
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── core
│   │   ├── package.json
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   └── web
│       ├── package.json
│       ├── src
│       │   └── index.ts
│       └── tsconfig.json
├── tsconfig.base.json
└── tsconfig.json
```

Before building your project, adjust the `main` property in the `package.json` of each package. Change `index.js` to `dist/index.js` since that's where it lives now.

Now build your project and run your CLI app:

```bash
npx tsc --build
node packages/cli/dist/index.js
```

You should see the usual output:

```text
Hello from Core!
Hello from CLI!
```

## You've done it! Good Job!

Now that you have laid the foundation for your project, go on and create something extraordinary!

The [project's source code](https://github.com/david-szabo97/example-typescript-npm-workspaces) is available on GitHub. The repository contains a few little changes. Feel free to explore!

If you would like to learn more about NPM workspaces and TypeScript, check out these links:
- [NPM Workspaces documentation](https://docs.npmjs.com/cli/v8/using-npm/workspaces)
- [TypeScript Project References documentation](https://www.typescriptlang.org/docs/handbook/project-references.html)

**PS.** [NX](https://nx.dev/) is an awesome tool! But sometimes, it's better to use the tools you already have at your disposal. Please take your time to decide whether to use NX or NPM / Yarn workspaces.