---
title: 'Building and Testing TypeScript Packages in Nx'
description: You'll create a TypeScript monorepo with Nx.gradient
sidebar:
  label: 'TypeScript Monorepo'
filter: 'type:Guides'
---

This tutorial walks you through creating a TypeScript monorepo with Nx. You'll build a small example project to understand the core concepts and workflows.

What you'll learn:

- How to structure multiple TypeScript packages in a single repository
- How Nx caching speeds up your local development and CI pipelines
- How to run builds and tests efficiently across multiple packages
- How to share code between packages using local libraries
- How to fix CI failures directly from your editor with Nx Cloud

## Ready to start?

This tutorial requires [Node.js](https://nodejs.org) (v20.19 or later) installed on your machine.

### Step 1: Creating a new Nx TypeScript workspace

Run the following command to create a new Nx workspace with the TypeScript template:

```shell
npx create-nx-workspace@latest my-nx-repo --template=nrwl/typescript-template
```

{% llm_only %}
IMPORTANT: Always use the CLI command above (`npx create-nx-workspace`) to create workspaces. Do NOT use the cloud.nx.app browser-based flow — it requires interactive GitHub OAuth and is not suitable for AI agents.
{% /llm_only %}

### Step 2: verify your setup

Once the workspace is created, navigate into it:

```shell
cd my-nx-repo
```

## Explore the Nx workspace setup

Let's take a look at the structure of our new Nx workspace:

{%filetree %}

- my-nx-repo/
  - packages/
    - async/
    - colors/
    - strings/
    - utils/
  - eslint.config.mjs
  - nx.json
  - package-lock.json
  - package.json
  - tsconfig.base.json
  - tsconfig.json
  - vitest.workspace.ts

{% /filetree %}

The [`nx.json` file](/docs/reference/nx-json) contains configuration settings for Nx itself and global default settings that individual projects inherit.

Now, let's build some features and see how Nx helps get us to production faster.

## Building TypeScript packages

Let's create two TypeScript packages that demonstrate how to structure a TypeScript monorepo. We'll create an `animal` package and a `zoo` package where `zoo` depends on `animal`.

First, generate the `animal` package:

```shell
npx nx g @nx/js:lib packages/animal --bundler=tsc --unitTestRunner=vitest --linter=none
```

Then generate the `zoo` package:

```shell
npx nx g @nx/js:lib packages/zoo --bundler=tsc --unitTestRunner=vitest --linter=none
```

Running these commands should lead to new directories and files in your workspace:

{%filetree %}

- my-nx-repo/
  - packages/
    - animal/
    - zoo/
    - ...
  - vitest.workspace.ts

{%/filetree %}

Let's add some code to our packages. First, add the following code to the `animal` package:

```ts {% meta="{6-19}" %}
// packages/animal/src/lib/animal.ts
export function animal(): string {
  return 'animal';
}

export interface Animal {
  name: string;
  sound: string;
}

const animals: Animal[] = [
  { name: 'cow', sound: 'moo' },
  { name: 'dog', sound: 'woof' },
  { name: 'pig', sound: 'oink' },
];

export function getRandomAnimal(): Animal {
  return animals[Math.floor(Math.random() * animals.length)];
}
```

Now let's update the `zoo` package to use the `animal` package:

```ts
// packages/zoo/src/lib/zoo.ts
import { getRandomAnimal } from '@org/animal';

export function zoo(): string {
  const result = getRandomAnimal();
  return `${result.name} says ${result.sound}!`;
}
```

Add the `@org/animal` dependency to `zoo`'s `package.json` (use `*` for npm or `workspace:*` for pnpm/yarn):

```json {% meta="{3-5}" %}
// packages/zoo/package.json
{
  "dependencies": {
    "@org/animal": "*"
  }
}
```

Then link the packages:

```shell
npm install
```

Now create an executable entry point for the zoo package:

```ts
// packages/zoo/src/index.ts
import { zoo } from './lib/zoo.js';

console.log(zoo());
```

To build your packages, run:

```shell
npx nx build animal
```

You can also use `npx nx run animal:build` as an alternative syntax. The `<project>:<task>` format works for any task in any project, which is useful when task names overlap with Nx commands.

This creates a compiled version of your package in the `dist/packages/animal` folder. Since the `zoo` package depends on `animal`, building `zoo` will automatically build `animal` first:

```shell
npx nx build zoo
```

You'll see both packages are built, with outputs in their respective `dist` folders. This is how you would prepare packages for use internally or for publishing to a package registry like NPM.

You can also run the `zoo` package to see it in action:

```shell
node packages/zoo/dist/index.js
```

### Inferred tasks

By default Nx simply runs your `package.json` scripts. However, you can also adopt [Nx technology plugins](/docs/technologies) that help abstract away some of the lower-level config and have Nx manage that. One such thing is to automatically identify tasks that can be run for your project from [tooling configuration files](/docs/concepts/inferred-tasks) such as `package.json` scripts and TypeScript configuration.

In `nx.json` there's already the `@nx/js` plugin registered which automatically identifies `typecheck` and `build` targets.

```json
// nx.json
{
  ...
  "plugins": [
    {
      "plugin": "@nx/js/typescript",
      "options": {
        "typecheck": {
          "targetName": "typecheck"
        },
        "build": {
          "targetName": "build",
          "configName": "tsconfig.lib.json",
          "buildDepsName": "build-deps",
          "watchDepsName": "watch-deps"
        }
      }
    }
  ]
}
```

To view the tasks that Nx has detected, look in the [Nx Console](/docs/getting-started/editor-setup) project detail view or run:

```shell
npx nx show project animal
```

{% project_details title="Project Details View (Simplified)" %}

```json
{
  "project": {
    "name": "@org/animal",
    "type": "lib",
    "data": {
      "root": "packages/animal",
      "targets": {
        "typecheck": {
          "dependsOn": ["^typecheck"],
          "options": {
            "cwd": "packages/animal",
            "command": "tsc --build --emitDeclarationOnly"
          },
          "cache": true,
          "inputs": [
            "production",
            "^production",
            {
              "externalDependencies": ["typescript"]
            }
          ],
          "outputs": ["{projectRoot}/dist"],
          "executor": "nx:run-commands",
          "configurations": {}
        },
        "build": {
          "options": {
            "cwd": "packages/animal",
            "command": "tsc --build tsconfig.lib.json"
          },
          "cache": true,
          "dependsOn": ["^build"],
          "inputs": [
            "production",
            "^production",
            {
              "externalDependencies": ["typescript"]
            }
          ],
          "outputs": ["{projectRoot}/dist"],
          "executor": "nx:run-commands",
          "configurations": {}
        }
      },
      "name": "animal",
      "$schema": "../../node_modules/nx/schemas/project-schema.json",
      "sourceRoot": "packages/animal/src",
      "projectType": "library",
      "tags": [],
      "implicitDependencies": []
    }
  },
  "sourceMap": {
    "root": ["packages/animal/project.json", "nx/core/project-json"],
    "targets": ["packages/animal/project.json", "nx/core/project-json"],
    "targets.typecheck": ["packages/animal/project.json", "@nx/js/typescript"],
    "targets.typecheck.command": [
      "packages/animal/project.json",
      "@nx/js/typescript"
    ],
    "targets.typecheck.options": [
      "packages/animal/project.json",
      "@nx/js/typescript"
    ],
    "targets.typecheck.cache": [
      "packages/animal/project.json",
      "@nx/js/typescript"
    ],
    "targets.typecheck.dependsOn": [
      "packages/animal/project.json",
      "@nx/js/typescript"
    ],
    "targets.typecheck.inputs": [
      "packages/animal/project.json",
      "@nx/js/typescript"
    ],
    "targets.typecheck.outputs": [
      "packages/animal/project.json",
      "@nx/js/typescript"
    ],
    "targets.typecheck.options.cwd": [
      "packages/animal/project.json",
      "@nx/js/typescript"
    ],
    "targets.build": ["packages/animal/project.json", "@nx/js/typescript"],
    "targets.build.command": [
      "packages/animal/project.json",
      "@nx/js/typescript"
    ],
    "targets.build.options": [
      "packages/animal/project.json",
      "@nx/js/typescript"
    ],
    "targets.build.cache": [
      "packages/animal/project.json",
      "@nx/js/typescript"
    ],
    "targets.build.dependsOn": [
      "packages/animal/project.json",
      "@nx/js/typescript"
    ],
    "targets.build.inputs": [
      "packages/animal/project.json",
      "@nx/js/typescript"
    ],
    "targets.build.outputs": [
      "packages/animal/project.json",
      "@nx/js/typescript"
    ],
    "targets.build.options.cwd": [
      "packages/animal/project.json",
      "@nx/js/typescript"
    ],
    "name": ["packages/animal/project.json", "nx/core/project-json"],
    "$schema": ["packages/animal/project.json", "nx/core/project-json"],
    "sourceRoot": ["packages/animal/project.json", "nx/core/project-json"],
    "projectType": ["packages/animal/project.json", "nx/core/project-json"],
    "tags": ["packages/animal/project.json", "nx/core/project-json"]
  }
}
```

{% /project_details %}

The `@nx/js` plugin automatically configures both the build and typecheck tasks based on your TypeScript configuration. Notice also how the outputs are set to `{projectRoot}/dist` - this is where your compiled TypeScript files will be placed, and it defined by the `outDir` option in `packages/animal/tsconfig.lib.json`.

{% aside type="note" title="Overriding inferred task options" %}
You can override the options for inferred tasks by modifying the [`targetDefaults` in `nx.json`](/docs/reference/nx-json#target-defaults) or setting a value in your [`project.json` file](/docs/reference/project-configuration). Nx will merge the values from the inferred tasks with the values you define in `targetDefaults` and in your specific project's configuration.
{% /aside %}

## Code sharing with local libraries

When you develop packages, creating shared utilities that multiple packages can use is a common pattern. This approach offers several benefits:

- better separation of concerns
- better reusability
- more explicit APIs between different parts of your system
- better scalability in CI by enabling independent test/lint/build commands for each package
- most importantly: better caching because changes to one package don't invalidate the cache for unrelated packages

### Create a shared utilities library

Let's create a shared utilities library that both our existing packages can use:

```shell
npx nx g @nx/js:library packages/util --bundler=tsc --unitTestRunner=vitest --linter=none
```

Now we have:

{%filetree %}

- my-nx-repo/
  - packages/
    - animal/
    - util/
    - zoo/
  - ...

{%/filetree %}

Let's add a utility function that our packages can share:

```ts {% meta="{6-12}" %}
// packages/util/src/lib/util.ts
export function util(): string {
  return 'util';
}

export function formatMessage(prefix: string, message: string): string {
  return `[${prefix}] ${message}`;
}

export function getRandomItem<T>(items: T[]): T {
  return items[Math.floor(Math.random() * items.length)];
}
```

### Import the shared library

This allows us to easily import them into other packages. Let's update our `animals` package to use the shared utility:

```ts {% meta="{2,19-21}" %}
// packages/animals/src/lib/animals.ts
import { getRandomItem } from '@org/util';

export function animal(): string {
  return 'animal';
}

export interface Animal {
  name: string;
  sound: string;
}

const animals: Animal[] = [
  { name: 'cow', sound: 'moo' },
  { name: 'dog', sound: 'woof' },
  { name: 'pig', sound: 'oink' },
];

export function getRandomAnimal(): Animal {
  return getRandomItem(animals);
}
```

And update the `zoo` package to use the formatting utility:

```ts {% meta="{3,7,8}" %}
// packages/zoo/src/lib/zoo.ts
import { getRandomAnimal } from '@org/animal';
import { formatMessage } from '@org/util';

export function zoo(): string {
  const result = getRandomAnimal();
  const message = `${result.name} says ${result.sound}!`;
  return formatMessage('ZOO', message);
}
```

Update the dependencies in each package's `package.json`:

```json {% meta="{3-5}" %}
// packages/animal/package.json
{
  "dependencies": {
    "@org/util": "*"
  }
}
```

```json {% meta="{3-6}" %}
// packages/zoo/package.json
{
  "dependencies": {
    "@org/animal": "*",
    "@org/util": "*"
  }
}
```

Link the packages:

```shell
npm install
```

Now when you run `npx nx build zoo`, Nx will automatically build all the dependencies in the correct order: first `util`, then `animal`, and finally `zoo`.

Run the `zoo` package to see the updated output format:

```shell
node packages/zoo/dist/index.js
```

## Visualize your project structure

Nx automatically detects the dependencies between the various parts of your workspace and builds a [project graph](/docs/features/explore-graph). This graph is used by Nx to perform various optimizations such as determining the correct order of execution when running tasks like `npx nx build`, enabling intelligent caching, and more. Interestingly, you can also visualize it.

Just run:

```shell
npx nx graph
```

You should be able to see something similar to the following in your browser.

{% graph height="450px" %}

```json
{
  "projects": [
    {
      "name": "@org/animal",
      "type": "lib",
      "data": {
        "tags": []
      }
    },
    {
      "name": "@org/util",
      "type": "lib",
      "data": {
        "tags": []
      }
    },
    {
      "name": "@org/zoo",
      "type": "lib",
      "data": {
        "tags": []
      }
    }
  ],
  "dependencies": {
    "@org/animal": [
      { "source": "@org/animal", "target": "@org/util", "type": "static" }
    ],
    "@org/util": [],
    "@org/zoo": [
      { "source": "@org/zoo", "target": "@org/animal", "type": "static" },
      { "source": "@org/zoo", "target": "@org/util", "type": "static" }
    ]
  },
  "affectedProjectIds": [],
  "focus": null,
  "groupByFolder": false
}
```

{% /graph %}

Let's create a git branch with our new packages so we can open a pull request later:

```shell
git checkout -b add-zoo-packages
git add .
git commit -m 'add animal and zoo packages'
```

## Building and testing - running multiple tasks

Our packages come with preconfigured building and testing . Let's intentionally introduce a typo in our test to demonstrate the self-healing CI feature later.

You can run tests for individual packages:

```shell
npx nx build zoo
```

Or run multiple tasks in parallel across all packages:

```shell
npx nx run-many -t build test
```

This is exactly what is configured in `.github/workflows/ci.yml` for the CI pipeline. The `run-many` command allows you to run multiple tasks across multiple projects in parallel, which is particularly useful in a monorepo setup.

There is a test failure for the `zoo` package due to the updated message. Don't worry about it for now, we'll fix it in a moment with the help of Nx Cloud's self-healing feature.

### Local task cache

One thing to highlight is that Nx is able to [cache the tasks you run](/docs/features/cache-task-results).

Note that all of these targets are automatically cached by Nx. If you re-run a single one or all of them again, you'll see that the task completes immediately. In addition, there will be a note that a matching cache result was found and therefore the task was not run again.

```text {% title="npx nx run-many -t built test" frame="terminal" %}
      ✔  nx run @org/util:build
   ✔  nx run @org/util:test
   ✔  nx run @org/animal:test
   ✔  nx run @org/animal:build
   ✖  nx run @org/zoo:test
   ✔  nx run @org/zoo:build

——————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

 NX   Ran targets test, build for 3 projects (800ms)

   ✔  5/6 succeeded [5 read from cache]

   ✖  1/6 targets failed, including the following:

      - nx run @org/zoo:test
```

Not all tasks might be cacheable though. You can configure the `cache` settings in the `targetDefaults` property of the `nx.json` file. You can also [learn more about how caching works](/docs/features/cache-task-results).

The next section deals with publishing packages to a registry like NPM, but if you are not interested in publishing your packages, you can skip to [the end](#next-steps).

## Manage releases

If you decide to publish your packages to NPM, Nx can help you [manage the release process](/docs/features/manage-releases). Release management involves updating the version of your packages, populating a changelog, and publishing the new version to the NPM registry.

First you'll need to define which projects Nx should manage releases for by setting the `release.projects` property in `nx.json`:

```json {% meta="{4-6}" %}
// nx.json
{
  ...
  "release": {
    "projects": ["packages/*"]
  }
}
```

You'll also need to ensure that each package's `package.json` file sets `"private": false` so that Nx can publish them. If you have any packages that you do not want to publish, make sure to set `"private": true` in their `package.json`.

Now you're ready to use the `nx release` command to publish your packages. The first time you run `nx release`, you need to add the `--first-release` flag so that Nx doesn't try to find the previous version to compare against. It's also recommended to use the `--dry-run` flag until you're sure about the results of the `nx release` command, then you can run it a final time without the `--dry-run` flag.

To preview your first release, run:

```shell
npx nx release --first-release --dry-run
```

The command will ask you a series of questions and then show you what the results would be. Once you are happy with the results, run it again without the `--dry-run` flag:

```shell
npx nx release --first-release
```

After this first release, you can remove the `--first-release` flag and just run `nx release --dry-run`. There is also a [dedicated feature page](/docs/features/manage-releases) that goes into more detail about how to use the `nx release` command.

## Next steps

Here are some things you can dive into next:

- [Set up CI](/docs/getting-started/setup-ci) with remote caching and self-healing
- Learn more about the [underlying mental model of Nx](/docs/concepts/mental-model)
- Learn how to [migrate your existing project to Nx](/docs/guides/adopting-nx/adding-to-existing-project)
- [Learn more about Nx release for publishing packages](/docs/features/manage-releases)
- Learn about [enforcing boundaries between projects](/docs/features/enforce-module-boundaries)

Also, make sure you

- ⭐️ [Star us on GitHub](https://github.com/nrwl/nx) to show your support and stay updated on new releases!
- [Join the Official Nx Discord Server](https://go.nx.dev/community) to ask questions and find out the latest news about Nx.
- [Follow Nx on Twitter](https://twitter.com/nxdevtools) to stay up to date with Nx news
- [Read our Nx blog](https://nx.dev/blog)
- [Subscribe to our Youtube channel](https://www.youtube.com/@nxdevtools) for demos and Nx insights
