In the previous article, we discussed how a monorepo structure could be package-centric or app-centric, depending on your project needs. Well, in this particular article, we will set up a monorepo using pnpm and take the app-centric route.
What's Pnpm?
Pnpm is a new generation of package management tools; it works just like NPM and yarn but offers some advantages.
## install pnpm
npm install -g pnpm # install pnpm globally
Create a new directory or clone an existing repository
mkdir monorepo # creates a new folder
or
git clone repository-link # clones a git repository (note: replace the link with repository URL)`
Let's change our current directory to the recently created directory or cloned repo.
cd monorepo # change directory
Next, we are going to initialize the package manager with pnpm.
pnpm init
Optional: Initialize git if the directory wasn't cloned from GitHub or GitLab.
git init # initializes git
touch .gitignore
Add node_modules and common build output folders to exclude pushing them to your repository.
# .gitignore
node_modules
dist
build
.env
Once all this is done, you can open your folder in your favorite code editor. If you use Visual Studio Code, you can open up the folder by using the command below
code . # opens up the folder in vs code
code . didn't work fix here
Set up the workspace
To set up a workspace, create a new file pnpm-workspace.yaml
touch pnpm-workspace.yaml # creates a new file
Add the configuration below to the workspace file:
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
This configuration tells pnpm that your workspace includes all directories in the apps and packages folders.
Create the apps and packages directory
mkdir -p apps packages
We will break down our apps into two apps/website
and apps/api
.
**apps/website
**The website directory, which we will configure using NextJS. To configure this, we need to change our current working directory to apps.
cd apps
pnpm create next-app website
apps/api
The api directory, which we will configure using NestJs.
# monorepo/apps (current working directory)
nest new api --package-manager=pnpm # creates a new NestJS project
Create a Shared UI Library
Great job! You have got your apps set up. Now, let's take things to the next level by creating a shared UI library. This is where the magic of monorepos starts to shine.
packages/library
The library directory will contain our shared UI components.
# monorepo
cd packages
mkdir library && cd library && pnpm init
# monorepo/packages/library/package.json
{
"private": true,
"name": "library",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Note we declare it as private by setting privateproperty to true; this is because we don't want to publish it to NPM or somewhere else, but rather just reference and use it locally within our workspace.
In our library folder, we will install the following: React as a dependency and TypeScript as a development dependency.
# monorepo
pnpm add --filter library react
pnpm add --filter library typescript -D
The --filter library
in the installation command, tells pnpm to install these NPM packages locally to the library package.
To keep things simple, we will make use of the TypeScript compiler to compile our package. We could have a more comprehensive arrangement for bundling numerous files together, using something like Rollup or Vite.
To use the TypeScript compiler, we are going to create a tsconfig.json file at the root of our library directory and add the configuration below.
# monorepo/packages/library/tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"module": "commonjs",
"outDir": "./dist"
},
"include": ["."],
"exclude": ["dist", "node_modules", "**/*.spec.ts"]
}
Next, we will update the package.json file main property to match the output directory specified in tsconfig.json
and also add a build script.
# monorepo/packages/library/package.json
{
"private": true,
"name": "library",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"scripts": {
"build": "rm -rf dist && tsc"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"react": "^18"
},
"devDependencies": {
"typescript": "^5.5.4"
}
}
Creating our first shared library UI component
Let us create a simple select component first Create the file in our library directory.
# monorepo/packages/library
touch Select.tsx
# monorepo/packages/library/Select.tsx
function CustomSelect(props: any) {
return <select onChange={(e) => props.onChange(e)}>
{props.children}
</select>;
}
export default CustomSelect;
We also want to have a public API where we export components to be used outside of our library package. To achieve this, we will create an index.tsx file and export the Select component we just created.
# monorepo/packages/library
touch index.tsx
# monorepo/packages/library/index.tsx
export * from './Select';
Building our library
pnpm --filter library build
Consuming our library package from the NextJs app
Congratulations! You just created your own shared UI library component. To use this library inside our NextJS app in the apps directory, you can add it using pnpm or manually.
pnpm add library --filter website --workspace
This adds the library package as a website dependency
{
"name": "website",
"version": "0.1.0",
"private": true,
...,
"dependencies": {
"library": "workspace:*",
...
},
"devDependencies": {
...
}
}
We are almost done. Let us update the library dependency value to "workspace:*"
. This means we want to resolve the package locally rather than from a registry like NPM, and * means we want to always use the latest version.
Now to use our Select Component, all we have to do is import it.
Start up the development server to see the component in action.
pnpm --filter website dev
Setting up scripts for the monorepo
In our root package.json
file, we are going to add some scripts to make our workflow easier.
# monorepo/package.json
"scripts": {
"dev": "pnpm run - parallel - filter \"./apps/**\" dev",
"build": "pnpm run - recursive - filter \"./apps/**\" build",
"test": "pnpm run - recursive - filter \"./apps/**\" test",
"lint": "pnpm run - recursive - filter \"./apps/**\" lint"
}
Congratulations! We did it. This process shows how to set up a monorepo using pnpm. For a real project, I would recommend using any of the monorepo tools available; I mentioned a few in my previous post. They offer some advantages over pnpm.