Editor’s note: This post was updated 31 January 2022 to use updated versions of Lerna and TypeScript, improve and better organize the tutorial, and provide clearer instructions for modern implementations.
It is often quite useful to use a single repository to maintain an entire project with all of the packages in it. Companies like Google and Facebook have used a single repository internally for most of their projects. This solution can be quite handy when a company uses a similar technology and when all projects share a common set of dependencies and files.
Popular JavaScript projects like Angular, React, Meteor, Babel, Nest.js and many others are using a single repository for all of their packages. In fact, some of them use Lerna for it.
What is Lerna?
Lerna is a popular and widely used tool written in JavaScript for setting and managing multi-package repositories for Node.js projects with npm and Git.
Lerna has two modes: fixed and independent. Fixed mode keeps all versions of packages at the same level. This approach is quite popular these days. You may have seen it in Angular.
Independent mode allows us to have different versions per package.
If you have a large or complex project, using a single repository for all its packages can help in organization and maintenance. Thankfully, it is quite easy with Lerna.
Initial setup
Before getting started, make sure you have npm and GitHub accounts because we’ll be using those to host our project. Also, create an npm organization so that you can publish the project with scoped packages once we’re done.
With this done, install Lerna as a global dependency:
npm install -g lerna
We’ll build a simple project. Ours will consist of multiple packages for our project, which we’ll call hospital-sdk
.
Create a folder named hospital
and initialize Lerna inside the folder:
lerna init && npm install
This command will create a lerna.json
file with a default folder structure in it.
/packages
is a placeholder for our shared packages, and lerna.json
is a Lerna configuration file. package.json
is our usual manifest file.
{ "packages": [ "packages/*" ], "version": "0.0.0" }
Lerna doesn’t create a .gitignore
file, so we will create one with this content:
node_modules/ lerna-debug.log npm-debug.log packages/*/lib .idea
We will use TypeScript in our project, but as we mentioned before, Lerna doesn’t support TypeScript, so we’ll treat it as a shared dependency. Add it on the top level of your package.json
with:
npm install typescript @types/node — save-dev
This is the recommended approach because we want to use the same tooling across our packages.
Preparing our build script
Alongside TypeScript, we will also install type declarations for Node.js. As Lerna is intended to be used with Node.js, not TypeScript, we will need to add some configuration to make it work.
We’ll have one common tsconfig.json
file defined in the root of our project. Our tsconfig.json
will look like:
{ "compilerOptions": { "module": "commonjs", "declaration": true, "noImplicitAny": false, "removeComments": true, "noLib": false, "emitDecoratorMetadata": true, "experimentalDecorators": true, "target": "es6", "sourceMap": true, "lib": [ "es6" ] }, "exclude": [ "node_modules", "**/*.spec.ts" ] }
Building our packages with lerna create
As this is a demo project, we will assume that we will have a few modules: patient
, doctor
, and scheduler
.
To create the packages, we’ll use the lerna create
command from the root of our project.
The lerna create
command will guide us through the creation of a new package. It requires your new package’s name to be passed as an argument. In this case, it’s doctor
, resulting in the full name being @hospital-sdk/doctor
.
We will repeat the same process for the patient
and scheduler
packages.
The resulting folder structure should now be:
Configuring TypeScript by file
Each individual package requires its own tsconfig.json
file for correct relative paths. Other options can be extended from the tsconfig.json
at the root of the project.
{ "extends": "../../tsconfig.json", "compilerOptions": { "outDir": "./lib" }, "include": [ "./src" ] }
After we add a tsconfig.json
to each package, we will create a src
folder inside each package with a TypeScript file for that package.
We also need to register a tsc
script in each individual package.json
. The result should be:
{ "name": "@hospital-sdk/doctor", "version": "0.0.0", "description": "> TODO: description", "author": "Vlado Tesanovic <[email protected]>", "homepage": "", "license": "ISC", "main": "lib/doctor.js", "typings": "lib/doctor.d.ts", "directories": { "lib": "lib", "test": "__tests__" }, "files": ["lib"], "publishConfig": { "access": "public" }, "scripts": { "tsc": "tsc", "test": "echo \"Error: run tests from root\" && exit 1" } }
We added simple logic in each .ts
file. We can test our setup by running:
lerna run tsc
The command above will run the tsc
script in all created packages:
If all goes well, we will compile TypeScript files from the src
folder into the lib
folder in each package.
Take a look at the package.json
for any package in our project and you’ll see attributes like directories
, files
, typings
, publishConfig
, and main
:
{ "name": "@hospital-sdk/doctor", "version": "0.0.0", "description": "> TODO: description", "author": "Vlado Tesanovic <[email protected]>", "homepage": "", "license": "ISC", "main": "lib/doctor.js", "typings": "lib/doctor.d.ts", "directories": { "lib": "lib", "test": "__tests__" }, "files": ["lib"], "publishConfig": { "access": "public" }, "scripts": { "tsc": "tsc", "test": "echo \"Error: run tests from root\" && exit 1" } }
The packages
, doctor
, and package.json
folders are very important because they control what will be pushed to npm, and will serve as the entry point for our library (main
and typings
attributes).
We will create a GitHub repository for this project and push all of the code there.
Linking packages
With TypeScript compiled, let’s create a test integration
package to see how Lerna handles linking packages.
cd packages mkdir integration cd integration npm init -y cd ../..
To install necessary dependencies from npm, or link ones from the monorepo, use the lerna add
command from the root of the project. In case of any naming conflicts, local packages will always take precedence over remote ones.
lerna add @hospital-sdk/doctor --scope=integration
The --scope
parameter specifies which module to add the package to.
More great articles from LogRocket:
- Don't miss a moment with The Replay, a curated newsletter from LogRocket
- Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
- Use React's useEffect to optimize your application's performance
- Switch between multiple versions of Node
- Discover how to animate your React app with AnimXYZ
- Explore Tauri, a new framework for building binaries
- Compare NestJS vs. Express.js
The unfortunate limitation of the lerna add
command is that it can only add one package at a time. Thus, if you need to install more dependencies at once, you can just add them to the package.json
and then run the lerna bootstrap
command.
{ "name": "integration", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "ts-node src/index.ts", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "ts-node": "^7.0.1", "@hospital-sdk/doctor": "^0.0.4", "@hospital-sdk/patient": "^0.0.4", "@hospital-sdk/scheduler": "^0.0.4" } }
lerna bootstrap
installs and links all dependencies, both local and remote. When using lerna add
, the lerna bootstrap
command is called automatically when adding any new packages.
To see whether packages were linked successfully, create a integration/src/index.ts
file with following content:
import { Doctor } from "@hospital-sdk/doctor"; import { Patient } from "@hospital-sdk/patient"; import { Scheduler } from "@hospital-sdk/scheduler"; console.log(new Doctor()); console.log(new Scheduler()); console.log(new Patient());
Then, add the start script in integration/package.json
.
{ "scripts": { "start": "ts-node src/index.ts" } }
Running the script from the terminal should produce the following output:
Doctor {} Scheduler {} Patient {}
Publishing to npm
Our goal is to publish all packages under the same npm scope, or organization. npm organizations can be private as well; in that case, you can control who is going to see and use your packages.
Earlier, we created a free, public organization at npmjs.org:
Login to that organization from the terminal:
At this moment, we have our organization and build scripts ready. Let’s glue it all together under one npm script from the root package.json
:
{ "scripts": { "publish": "lerna run tsc && lerna publish" } }
From the terminal, run:
npm run publish
Lerna will guide us through the publishing process. We will need to choose a package version and push tags to GitHub.
If all goes well, we will see this message at the end: lerna success published 3 packages.
npm run publish > publish > lerna run tsc && lerna publish lerna notice cli v3.4.0 lerna info Executing command in 3 packages: "npm run tsc" > @hospital-sdk/[email protected] tsc > tsc > @hospital-sdk/[email protected] tsc > tsc > @hospital-sdk/[email protected] tsc > tsc lerna success run Ran npm script 'tsc' in 3 packages: lerna success - @hospital-sdk/doctor lerna success - @hospital-sdk/patient lerna success - @hospital-sdk/scheduler lerna notice cli v3.4.0 lerna info Verifying npm credentials lerna info current version 0.0.3 lerna info Looking for changed packages since v0.0.3 ? Select a new version (currently 0.0.3) Patch (0.0.4) Changes: - @hospital-sdk/doctor: 0.0.3 => 0.0.4 - @hospital-sdk/patient: 0.0.3 => 0.0.4 - @hospital-sdk/scheduler: 0.0.3 => 0.0.4 ? Are you sure you want to publish these packages? Yes lerna info git Pushing tags... lerna info publish Publishing packages to npm... lerna notice ... lerna notice lerna info published @hospital-sdk/doctor 0.0.4 lerna info published @hospital-sdk/patient 0.0.4 lerna info published @hospital-sdk/scheduler 0.0.4 Successfully published: - @hospital-sdk/[email protected] - @hospital-sdk/[email protected] - @hospital-sdk/[email protected] lerna success published 3 packages
Conclusion
Setting up Lerna for TypeScript takes some configuration, but in the long run, will help you increase the maintainability of your packages, both public and private.
You can find the source code for this project at my GitHub, and learn more about Lerna in their docs.
LogRocket: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
200’s only
Monitor failed and slow network requests in production
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket. 
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
try nx insted of lerna
FYI lerna is not maintained anymore and should not be used.
See note in their site https://github.com/lerna/lerna