This repo is a step-by-step tutorial to setup a monorepo using Lerna and Yarn Workspaces. Each step builds on the previous and at the end you will have a monorepo setup with the following:
- Lerna
- Yarn Workspaces
- GraphQL Server
- React Web Application
- Shared Common Library
- Node
- Yarn
- Lerna -
npm install -g lerna
- If you don't want to install lerna on your machine, just run the lerna commands below prefixed with
npx
, like sonpx lerna init
.
- If you don't want to install lerna on your machine, just run the lerna commands below prefixed with
- Fork this repo
git checkout steps/0
;
- Each step below starts as if you are at the root of your project.
Branches
This step-by-step tutorial starts from scratch, but captures each step in a branch in case you get lost or want to skip forward.
Branches are structured like so: step/[step]
, for example step/1
would bring you to the final state of step 1 and would be the starting point for step 2.
Emojis
Emojis are used throughout this tutorial to indicate specific things, as outlined below.
π - Tips and experiments that are optional and not required to move on to the next step.
π - A command that should be ran as part of the step.
βοΈ - An edit to a file
π - Something you should look at for clarity or verification.
Lets get Lerna setup and use some Lerna commands to scaffold out our monorepo.
- π
lerna init
- π
lerna create common --yes
- π
lerna create server --yes
- π
lerna create client --yes
- π
lerna exec -- touch index.js
to generate a placeholder index file. - π
lerna add common --scope={client,server}
- π
lerna bootstrap
from the root - Verify symlinks have been created
- If things worked correctly the common package should now be symlinked to server and client. Now we can share code between packages within the same repo. Neat!
- One easy way to see this is
tree
, install on OSX viabrew install tree
.
- One easy way to see this is
- If things worked correctly the common package should now be symlinked to server and client. Now we can share code between packages within the same repo. Neat!
Some helpful commands:
lerna bootstrap
- Create symlinks between package dependencies and installs 3rd party dependencies for each package.lerna create
- Adds a package to your monorepo.lerna add
- Adds a dependency to all your packages.lerna list
- Shows your packageslerna run
- Runs a command in each of your packages; this is really nice for running tests.- For example:
lerna run test
- For example:
lerna exec
- Runs a command for each package. Note that you need to provide--
in front of the command.- For example:
lerna exec -- touch index.js
- For example:
- We'll get to publishing, diffs, etc later.
Some helpful flags:
--scope
- Allows you so specify what packages to run the command for- Single package example:
lerna add react --scope=client
- Multiple packages example:
lerna add ramda --scope={client,server}
- Single package example:
For a full list of commands and flags: click here
Now that we have lerna setup, let's take advantage of a feature known as hoisting.
- π
lerna add ramda
from the root - π
lerna bootstrap
to install ramda- π at each of your package's
package.json
andnode_modules
. Notice thatramda
is in each of these.
- π at each of your package's
- π
lerna bootstrap --hoist
- π again and notice that
ramda
is still in your packagespackage.json
but is no longer in eachnode_modules
, rather it is in one place; at the rootnode_modules
.
- π again and notice that
Hoisting is really nice, because it allows you to install shared dependencies in one node_modules
verse having multiple copies of it throughout your packages.
If you did not complete the previous step or want to start fresh: π
git checkout steps/1
.
Yarn Workspaces allows us:
- To install all over dependencies across all packages in one top level
node_modules
folder, reducing the overall size of ournode_modules
folders that otherwise would live inside each of our packages. - Have a single
yarn.lock
which will reduces the chances for conflicts and makes reviews easier. - Eliminate the need to hoist dependencies as we did in the previous step.
We are going to tell Lerna to use Yarn and also that we want to enable Yarn's workspaces feature.
- βοΈ Add the following to the root
package.json
"private": true, "workspaces": ["packages/*"]
- βοΈ Add the following to
lerna.json
"useWorkspaces": true, "npmClient": "yarn"
- π
lerna clean
- This removes the existing
node_modules
folders.
- This removes the existing
- π
lerna exec -- rm -f ./package-lock.json
- This removes the existing
package-lock.json
files.
- This removes the existing
- π
lerna bootstrap
from the root- π at the root
node_modules
andyarn.lock
. More importantly, notice that there are nonode_modules
in our packages nor are there any lock files.
- π at the root
Now that we have Yarn Workspaces setup, let's experiment with adding a new dependency.
- π
lerna add ramda
from the root - π
lerna bootstrap
to install ramda- π at your
node_modules
and each package'spackage.json
. Notice thatramda
has been added for each project and there is only onenode_modules
folder at the root.
- π at your
By adding Yarn Workspaces we've eliminated the need to hoist up dependencies manually and just let Yarn Workspaces manage that for us. How nice!
At this point, you have a monorepo ready to go. The next steps present a fairly opinionated way to setup a full stack application with GraphQL (server) and React (client) who both use a shared library (common).
If you did not complete the previous step or want to start fresh: π
git checkout steps/2
.
We are going to scaffold out a React app using a tool called create-react-app. This was built by facebook to provide a standardized way to quickly setup a React app. One of the main advantages is that it shields you from all the configuration that goes along with setting up a React app from scratch.
- π
rm -rf ./packages/client
- This kills off the existing client folder
- π
npx create-react-app --scripts-version @react-workspaces/react-scripts ./packages/client
- This will scaffold out a react app using create-react-app and a custom version of react-scripts that adds support for Yarn Workspaces to Create React App.
- π
rm -f yarn.lock && rm -f ./packages/client/yarn.lock
- This kills off any existing lock files that could potentially conflict with our top level lock file
- π
rm -rf ./packages/client/node_modules
- This kills off the
node_modules
folder in the client package because we want to load them from the root after we run the subsequent command to bootstrap our monorepo.
- This kills off the
- π
lerna bootstrap
- Installs react app dependencies in top level
node_modules
- Installs react app dependencies in top level
- π
cd ./packages/client && yarn start
You should now have a React app running at this URL: http://localhost:3000
If you did not complete the previous step or want to start fresh: π
git checkout steps/3
.
We will be setting up our server using GraphQL, which in short is a query language for APIs and a runtime for fulfilling those queries with your existing data.
Similar to how we scaffolded out a React app, there are tools out there that make it easy to setup a GraphQL server vs building one from scratch. We will be using a tool called GraphQL Yoga, which is one of the most popular options with over 5k stars on GitHub.
-
π
lerna add graphql-yoga --scope=server
- You should now see
graphql-yoga
in yourserver
packagespackage.json
file 2 βοΈ Add the following topackages/server/index.js
:
import { GraphQLServer } from 'graphql-yoga'; const typeDefs = ` type Query { hello(name: String): String! } `; const resolvers = { Query: { hello: (_, { name }) => `Hello ${name || 'World'}`, }, }; const server = new GraphQLServer({ typeDefs, resolvers }); server.start(() => console.log('Server is running on localhost:4000'));
- You should now see
-
π
lerna add nodemon --scope=server --dev
-
π
lerna add esm --scope=server
-
π
lerna bootstrap
-
π
cd ./packages/server
-
π
npx nodemon -r esm ./
You should now have a GraphQL server running at this URL: http://localhost:4000
Let's open up our GraphQL server by going to http://localhost:4000 in web browser.
You will notice that there is some cool looking tool there. That is called GraphiQL, a tool that allows you to query your GraphQL server.
In the left pane, you'll want to enter the following:
query hello($name: String) {
hello(name: $name)
}
Next, we will need to provide our query with values for name
. Click the "QUERY VARIABLES" link at the bottom left and enter the following:
{
"name": "Human"
}
Click the play button and you should see something like this:
{
"data": {
"hello": "Hello Human"
}
}