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?
Section titled “Ready to start?”This tutorial requires Node.js (v20.19 or later) installed on your machine.
Step 1: Creating a new Nx TypeScript workspace
Section titled “Step 1: Creating a new Nx TypeScript workspace”Run the following command to create a new Nx workspace with the TypeScript template:
npx create-nx-workspace@latest my-nx-repo --template=nrwl/typescript-templateOr create your workspace in the browser with CI pre-configured.
Step 2: verify your setup
Section titled “Step 2: verify your setup”Once the workspace is created, navigate into it:
cd my-nx-repoExplore the Nx workspace setup
Section titled “Explore the Nx workspace setup”Let's take a look at the structure of our new Nx workspace:
Directorymy-nx-repo/
Directorypackages/
Directoryasync/
- …
Directorycolors/
- …
Directorystrings/
- …
Directoryutils/
- …
- eslint.config.mjs
- nx.json
- package-lock.json
- package.json
- tsconfig.base.json
- tsconfig.json
- vitest.workspace.ts
The nx.json file 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
Section titled “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:
npx nx g @nx/js:lib packages/animal --bundler=tsc --unitTestRunner=vitest --linter=noneThen generate the zoo package:
npx nx g @nx/js:lib packages/zoo --bundler=tsc --unitTestRunner=vitest --linter=noneRunning these commands should lead to new directories and files in your workspace:
Directorymy-nx-repo/
Directorypackages/
Directoryanimal/
- …
Directoryzoo/
- …
- ...
- vitest.workspace.ts
Let's add some code to our packages. First, add the following code to the animal package:
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:
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):
{ "dependencies": { "@org/animal": "*" }}Then link the packages:
npm installNow create an executable entry point for the zoo package:
import { zoo } from './lib/zoo.js';
console.log(zoo());To build your packages, run:
npx nx build animalYou 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:
npx nx build zooYou'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:
node packages/zoo/dist/index.jsInferred tasks
Section titled “Inferred tasks”By default Nx simply runs your package.json scripts. However, you can also adopt Nx technology plugins 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 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.
{ ... "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 project detail view or run:
npx nx show project animal@org/animal
Root: packages/animal
Type:library
Targets
build
tsc --build tsconfig.lib.json
Cacheabletypecheck
tsc --build --emitDeclarationOnly
Cacheable
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.
Code sharing with local libraries
Section titled “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
Section titled “Create a shared utilities library”Let's create a shared utilities library that both our existing packages can use:
npx nx g @nx/js:library packages/util --bundler=tsc --unitTestRunner=vitest --linter=noneNow we have:
Directorymy-nx-repo/
Directorypackages/
Directoryanimal/
- …
Directoryutil/
- …
Directoryzoo/
- …
- ...
Let's add a utility function that our packages can share:
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
Section titled “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:
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:
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:
{ "dependencies": { "@org/util": "*" }}{ "dependencies": { "@org/animal": "*", "@org/util": "*" }}Link the packages:
npm installNow 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:
node packages/zoo/dist/index.jsVisualize your project structure
Section titled “Visualize your project structure”Nx automatically detects the dependencies between the various parts of your workspace and builds a project 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:
npx nx graphYou should be able to see something similar to the following in your browser.
Let's create a git branch with our new packages so we can open a pull request later:
git checkout -b add-zoo-packagesgit add .git commit -m 'add animal and zoo packages'Building and testing - running multiple tasks
Section titled “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:
npx nx build zooOr run multiple tasks in parallel across all packages:
npx nx run-many -t build testThis 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
Section titled “Local task cache”One thing to highlight is that Nx is able to cache the tasks you run.
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.
✔ 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:testNot 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.
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.
Manage releases
Section titled “Manage releases”If you decide to publish your packages to NPM, Nx can help you manage the release process. 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:
{ ... "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:
npx nx release --first-release --dry-runThe 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:
npx nx release --first-releaseAfter this first release, you can remove the --first-release flag and just run nx release --dry-run. There is also a dedicated feature page that goes into more detail about how to use the nx release command.
Next steps
Section titled “Next steps”Here are some things you can dive into next:
- Set up CI with remote caching and self-healing
- Learn more about the underlying mental model of Nx
- Learn how to migrate your existing project to Nx
- Learn more about Nx release for publishing packages
- Learn about enforcing boundaries between projects
Also, make sure you
- ⭐️ Star us on GitHub to show your support and stay updated on new releases!
- Join the Official Nx Discord Server to ask questions and find out the latest news about Nx.
- Follow Nx on Twitter to stay up to date with Nx news
- Read our Nx blog
- Subscribe to our Youtube channel for demos and Nx insights