4 Simple Steps to Streamline Your Development: Creating a Monorepo Setup with Nx & Yarn Workspaces.
With the rise of efficient build systems like Nx and Turborepo, creating and managing monorepos have become simpler.
Let's create a simple monorepo setup that has a NestJS project and NextJS project with shared linting, type-checking, formatting, and caching capabilities.
1. Setup yarn workspaces
Create a folder and open it with VSCode.
mkdir next-nest-monorepo
code next-nest-monorepo // Needs code command to be installed in path in vscode. Or manually open the folder.
yarn init -y
mkdir apps libs // Create folders for apps and libs
Update the package.json of the newly created nodejs project with the below.
{
"name": "next-nest-monorepo",
"version": "1.0.0",
"license": "MIT",
"private": "true",
"workspaces": {
"packages": [
"apps/*",
"libs/*"
]
}
}
"private": "true"
indicates that the monorepo is private, and it won't be published to a public registry like npm."workspaces": {"packages": ["apps/*", “libs/*”]}
specifies the location of the packages in the monorepo. Here, the"apps/*"
pattern indicates that all packages in theapps
directory should be included as workspaces. Workspaces are a Yarn feature that allows you to manage multiple packages in a single repository.
Git
Initialize and do the first commit.
git init
// Create a .gitignore
echo "node_modules \nbuild \ndist \n.next \n.env" >> .gitignore
git add .
git commit -m 'initial commit'
Create projects in “apps/*”
Create an apps directory and create NextJS and NestJS applications inside it.
// Create apps directory.
mkdir apps
cd apps
// Next
yarn create next-app web --ts
// Nest
npx nest new api --package-manager=yarn
Some fixes
Now we have a yarn workspace setup with both our backend and frontend projects inside it. I had to address a few problems.
If you face issues working with nestjs in a monorepo, you may need to avoid adding nestjs dependencies to the root node_modules. You can update the workspaces in the package.json as below.
"workspaces": {
"packages": [
"apps/*"
],
"nohoist": [
"**/@nestjs",
"**/@nestjs/**"
]
}
“nohoist” enables workspaces to consume 3rd-party libraries not yet compatible with its hoisting scheme. The idea is to disable the selected modules from being hoisted to the project root. They were placed in the actual (child) project instead, just like in a standalone, non-workspaces, project.
I also found that the NestJS project creates a separate .git when we initialize it. Remove it.
cd apps/api
rm -rf .git
2. Setup Nx: The build system
We are going to use Nx as the build system for our yarn monorepo.
npx add-nx-to-monorepo
Answer the questionnaire. I also answered yes to “Enable distributed caching to make your CI faster?”.
This creates a nx.json
file in the root.
{
"tasksRunnerOptions": {
"default": {
"runner": "@nrwl/nx-cloud",
"options": {
"cacheableOperations": ["build"],
"accessToken": "******"
}
}
},
"defaultBase": "master"
}
Let’s try using the Nx commands. The below one helps us to run all build
commands in our monorepo.
Executing commands with Nx
yarn nx run-many --target=build
Now run the command again.
The built time for the two applications went from to 0.48 seconds
to 11.75 seconds
How cool is that? 😎⚡️
3. Validation: Typescript | ESLint | Prettier
To ensure that all of our projects are validated uniformly, create a validation script in our root directory.
Prettier
Install prettier. We need to add — ignore-workspace-root
or -W
to ignore the workspace root check.
yarn add -D prettier -W
Create a .prettierrc
file in the root.
{
"singleQuote": true,
"trailingComma": "all",
"semi": false
}
Prettier plugins:
I also added prettier-plugin-organize-imports
plugin that helps remove unused imports and sort imports on save. It is a huge time save for me.
yarn add -D prettier-plugin-organize-imports
Automatic plugin discovery happens for prettier < 3. For version above 2, refer to the documentation.
Format scripts
Add scripts in the root package.json
file for checking and fixing format issues.
"scripts": {
"prettier": "prettier \"{apps,libs}/**/*.{ts,tsx,js,json}\" --ignore-path .gitignore",
"format:check": "yarn prettier --check",
"format:write": "yarn prettier --write",
}
Now we can run formatting for the whole monorepo from the root.
yarn format:check
yarn format:write
Type-checking
Add the below tsc
commands in the respective files. You will also need to add tsc script to the package.json of the libs you create.
// apps/api/package.json -> "scripts"
"tsc": "tsc"
// apps/web/package.json -> "scripts"
"tsc": "tsc"
// package.json -> "scripts"
"tsc": "nx run-many --target=tsc"
Now, we can enjoy type-checking the whole monorepo from the root.
yarn tsc
Linting
Both our nestjs and nextjs have eslint setup for us with alint
script. Add a common lint script with Nx’s run-many feature in the root package.json.
"lint": "nx run-many --target=lint",
yarn lint // Runs linting in both our projects.
Run them all parallel together
In huge projects, each of these commands can take a significant amount of time. Let's create a validate
script that runs all three scripts in parallel. You can add other scripts as well liketest
.
Install npm-run-all
in the root.
yarn add -D npm-run-all -W
Add validate script in the root package.json file.
"validate": "run-p lint tsc format:check",
Now the scripts look like below.
"scripts": {
"prettier": "prettier \"{apps,libs}/**/*.{ts,tsx,js,json}\" --ignore-path .gitignore",
"format:check": "yarn prettier --check",
"format:write": "yarn prettier --write",
"lint": "yarn nx run-many --target=lint",
"tsc": "yarn nx run-many --target=tsc",
"prevalidate": "yarn format:write",
"validate": "run-p format:check lint tsc"
}
Note I also added a prevalidate script that runs before the validate which runs format:write for us. At this point we can remove the format:check in validate. But I’m going to leave it there.
Run one command, sit back, and enjoy the automation 😎.
yarn validate
It all took 3.5 seconds
.
Want to optimize more?
I added lint
and tsc
to the “cacheableOperations”
in nx.json
.
{
"tasksRunnerOptions": {
"default": {
"runner": "@nrwl/nx-cloud",
"options": {
"cacheableOperations": ["build", "lint", "tsc"],
"accessToken": "******"
}
}
},
"defaultBase": "master"
}
Our
validate
script went from 3.5 seconds to 0.74 seconds.
Learn how Nx does the caching in this official documentation.
4. Run validation on the Pre-commit hook.
We have a powerful validation script. Let's run that before every commit by usinghusky
and lint-staged
.
Initialize husky.
npx husky-init && yarn
This will create a .husky
folder with pre-commit
hook.
Install lint-staged in the root.
yarn add -D lint-staged -W
Create a lint-staged.config.js
in the root.
module.exports = {
'*.{ts,tsx}': (filenames) => ['yarn validate'],
}
Notice that this also runsformat:write
before validating due to the prevalidate script.
Modify the pre-commit
file in .husky
to below.
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
That’s all. Please feel free to play around inducing type, linting, and formatting issues anywhere in the codebase and see if you can commit.
I created a type/lint error in my _app.tsx file.
const a = 5
a = 'str'
Then, I tried to commit
git add .
git commit -m 'testing pre-commit'
The commit fails as expected. 😎
This drastically improves the quality of commits and stops dirty commits in the developers' local environment.
Fix the type error and your commit will go through.
Fire exit?
What if your building is on fire and you are stuck with a type error that stops you from pushing your code?
Use --no-verify
.
git commit -m 'testing no-verify' --no-verify
This will skip the pre-commit validation altogether in emergency situations.
Conclusion
We have done a lot today. We created a mono repo. Set up Nx build system to run cacheable commands. We created a validate
script with a pre-commit
hook facility. All these setups will help us keep all our projects in one place safe. And safe codebases make happy developers.
Happy coding! 🙌