Why you should switch from Lerna to Nx - Nrwl
If you haven’t used Nx, you way want to watch this 10-minute video before reading the rest of the post:
Creating Applications
Now, let’s create a new Nx workspace.
npx create-nx-workspace happyorg --preset=react-express --appName=tickets
The workspace has two applications tickets
and api
, a suite of e2e tests, and a library api-interfaces
, which the applications share.
We can run the applications as follows:
nx serve api
nx serve tickets
If we change anything in api-interfaces
both the api and the frontend will reflect the change.
The example shows that Nx provides first class support not just for libraries (packages to be used in other packages), but also for applications. We can build/serve/test them. Applications often work in tandem, and Nx helps coordinate that. Application can often run in different modes, and Nx provides a way to manage those (e.g., nx serve tickets
and nx serve tickets --configuration=production
).
Creating Libraries
Most libraries in an org monorepo are consumed by other libraries and applications in the same repo. This means they are lightweight, which in turn means we can create many more of them.
Creating every single library from scratch is tedious and error prone. That’s why Nx has first-class plugins for a few major frameworks, so we can create plain ts/js libraries, angular libraries, react libraries with ease. This is how it is done:
nx g @nrwl/react:lib ticket-list
Right after we ran the command, we can import the newly created library into an application without any configuration:
Having such generic capabilities is good, but not enough. Every organization has specific needs. With Nx, we can define our unique set of code generators we can use to promote best practices within our organization.
Enforcing Ownership
Let’s say we have a different team that added another application into the workspace.
yarn add @nrwl/angular
nx g @nrwl/angular:app agent
We need to make sure that the agent and tickets teams can develop their applications without stepping on each others toes.
Adding a CODEOWNERS file solves a part of the problem, so the folks from the agent team cannot change the tickets app without the tickets team knowing and vise versa. That’s good but not enough.
To see why, let’s imagine the api-interfaces
library is owned by the tickets team, and they want to be able to change it freely. As it stands right now, nothing prevents the agent team from doing this:
This is a problem.
The CODEOWNERS file tells us who owns every node of the dependency graph, but it doesn’t tell us what edges are allowed. To solve this, Nx provide a generic mechanism for defining rules that specify how libraries can depend on each other. So the import above will result in this error message:
Read more about it here.
Building Only What is Affected
Rebuilding and retesting everything on every commit does not scale beyond a handful of projects.
Nx can figure out the dependency graph of the workspace by analyzing the code. It then can use the graph to only rebuild and retest what is affected.
For instance, if we change the api-interfaces
library and run:
nx affected:dep-graph
We will see that Nx figured out that both api
and tickets
are affected by this change:
We can test what is affected, like this:
nx affected:test --parallel # runs tests for api-interfaces
, api
, and tickets
in parallel.
We can also easily distribute the tests across a grid of machines on CI.
Read more about it here.
Variability
Even our tiny workspace already has a bunch of technologies in it.
apps/ agent/ - Angular app agent-e2e/ - Cypress tests api/ - Express app tickets/ - React apptickets-e2e/ - Cypress tests
libs/ api-interfaces/ - simple TypeScript lib ticket-list/ - a React lib...
Even though Nx uses Webpack, Cypress, Jest, ESLint under the hood, it provides a unified command-line interface, so we can interact with all the projects in the same way.
nx lint agent
will use tslint, while nx lint tickets
will use eslint. nx serve api
will start the express app, and nx serve tickets
will start webpack dev server. Each of those commands can have a completely different implementation with different flags. So we don’t lose any flexibility, we merely standardize how we invoke the commands.
This is what allows nx affected
to work. Running nx affected:lint
will relint the affected libraries and applications and will use the appropriate linter for each project. Similarly, nx affected:test
will retest what is affected and will use the appropriate test runner where needed.
It also helps with developer mobility. One of the benefits of a monorepo is that folks can reuse each other’s code and contribute to each other’s projects. Being able to discover what commands a project has, and how to serve/test/build
it, is key for enabling it.