A boilerplate for a fullstack monorepo in TypeScript with Create React App on the client, and Node.js (express) on the server. Uses Lerna for monorepo functionality.
Based off https://github.com/michaljach/modern-monorepo-boilerplate
This was created because the above boilerplate:
- Doesn't include a template package for a Node.js server
- Doesn't include GitHub Action CI/CD integrations
- Doesn't include built-in Heroku deployment configurations
- Boilerplate Mono-repo in TypeScript with Create React App and Node.js
- Contents
- Stack and features
- Usage
- Use cases for this boilerplate
- Limitations
Main technologies in this stack included:
Additional technologies include:
- Express
- Eslint
- Jest
- Prettier
- Nodemon
For the server side, we are using Express, but that can be easily swapped out for other frameworks if needed.
Additional CI/CD functionality include:
- Automated testing on commits (via GitHub Actions)
- Config files for easy integration and deployment with Heroku
When deployed to Heroku, the app hosted is the Node.js server, that when connected to, serves the client and allows usage of the client app on the cloud.
Prior to any usage, you should learn the concepts of Lerna as the whole monorepo revolves around this library. Otherwise if you don't, you may not fully understand the structure of the monorepo and how it operates.
- Clone the repo or simply paste all the files into an existing or newly created repo
- Run
yarn install
- Run
yarn bootstrap
- Run
yarn start
It should then proceed to open a new window in your browser on
localhost:3000
where you should see a working page. If the client
successfully connected to the server, you should see a Hello from server!
text appear below the title. If it's stuck on Loading...
, then the client
somehow can't access the server.
After the initial setup, you can run the other commands for additional functionality.
The build script can be called manually to build all the packages and create
output JavaScript files based on the TypeScript files. The output files are
generated in each package, in a build/
folder.
This is also called automatically when needed during CI/CD.
When doing development, you'll be running this as usual. This should run all the packages together, where you'll have both the client and server deployed at the same time. The client can then interact with the server.
The client runs on localhost:3000
. The server runs on localhost:3001
but
will only work if the project was built at least once before with yarn build
(see above). Checking the server gives you an idea of how it will
look/behave during actual deployment. Create React App hot reloading will
only work on port 3000
.
Development UX was considered into this monorepo, so that any changes to the client or server should automatically build and you can see live changes in the browser. Client hot reloading is handled by Create React App, whereas server reloading is handled by Nodemon.
Tests for all the packages can be run at any time with this command.
I hardly use yarn to add npm packages, since it may mess with Lerna.
Typically if I want to add a npm package, it's for a single package.
Definition of npm package versus Lerna package:
- npm package: A typical npm package that's installed via
npm
oryarn
in thenode_modules/
folder. If you've worked with node.js before, you already know what this is. - Lerna "package": Basically a sub-repo within this monorepo. These are the folders inside the
project/packages/
folder.
The folder structure for your packages would look something like:
packages/
client/
components/
server/
So in this example, you have 3 Lerna "packages", or 3 sub-repos. You will normally have hundreds of ordinary npm packages sitting in various node_modules/
folders throughout the monorepo. There's a node_modules/
folder in each package/sub-repo, and one in the project root, for the monorepo itself.
Normally you'd just run npm install [package-name]
or yard add [package-name]
. This isn't a typical repo, since this is a monorepo. First, we are using yarn so might as well use yarn for this monorepo. Second, Lerna likes to do most of the management between various packages and their dependencies. It's very messy. That's why we hardly ever want to use the npm
or yarn
commands.
Instead we'll be using lerna
, as described in more detail below.
How do we do it? Simply with lerna add [package-name]
. But that's not good, we want more specific commands:
lerna add [package-name] --scope=[@namespace/package]
The scope is important, as we want to only install a specific npm package into that package/sub-repo.
Where do we get the scope name? It's not the folder name, but instead the "name"
value inside that package's package.json
file.
In this monorepo, the name for the client, would be @namespace/client
or something similar to that.
A full example command would be:
lerna add lodash --scope=@namespace/client
You can add the --dev
flag at the end to add it as a development dependency. It'll get added to the appropriate dev dependency section of the package's package.json
file.
Note: especially with an IDE, you may need to also add the typescript types to the package. But you'll need to run it with the --dev
flag.
For example:
lerna add @types/pino --scope=@namespace/server --dev
You have to run yarn add [package-name] -W
within the root project directory. If you don't add the -W
flag, it won't let you do it, and it'll ask you to type in the same command again but with that flag.
Some use cases I used this with are related to configuring eslint
with the whole repo, such as:
yarn add eslint-config-airbnb-typescript --dev -W
yarn add @typescript-eslint/parser --dev -W
Sometimes you just tinker around and end up manually updating the versions in a package's package.json
file and run yarn install
. This behaviour may break Lerna and make things buggy.
So how do we fix this? After doing any sort of manual fixing/updating of npm packages, we use the lerna bootstrap
command.
In a package/sub-repo that you made manual changes to the npm packages in, run:
lerna bootstrap --scope=[package-name]
Again, ensure that in the scope field, you enter the package name, which is found in the package's package.json
file, under the "name"
field.
A working example command would look like:
lerna bootstrap --scope=@namespace/client
This basically runs yarn's install functionality, but also manages lerna's records of the different npm packages, their versions and where they are installed.
You can always run lerna boostrap
itself without a --scope
flag, that can work too, and it "fixes" all the packages in the monorepo.
I can't fully remember at this time of writing, but I think you can just yarn remove
the package you want, then just run lerna bootstrap
afterwards to "fix it". Lerna unfortunately doesn't have a lerna remove
function at this point of time, idk why. See the issue on the lerna repo here: lerna/lerna#1886
This project can be automatically deployed on a Heroku application. Simply
head to your Heroku app, go to the Deploy
tab, then enable Automatic
Deploys. Tick the Wait for CI to pass before deploy
option too.
By default this will deploy the server/
package on the app.
This boilerplate was originally intended to be used for a "fullstack" client for a web application. I needed a way to have both the front end code (React) be in the same repo as the server code (middleware GraphQL client, Apollo). My "client server" is a server that primarily handled database interactions on behalf of the database itself, that served the web app (client). Thus I required a monorepo to put the two original repos (client + server) together as they require each other for deployment.
The monorepo is intended to be deployed on Heroku, but is limited to only 1 of the packages that can be deployed. See limitations section below.
Additional packages can be added to the boilerplate, such as:
- Storybook prototyping
- Other side clients and servers
- etc.
I typically used npm entirely for my previous projects, but now I'll have to stick with yarn as this whole setup requires the use of yarn's workspace feature.
Lerna is required for monorepo functionality in general for Javascript based projects.
You may want to learn how these technologies work.
Note: This limitation only applies if you're deploying on Heroku normally
Given how Heroku deploys projects automatically via a Procfile
, Heroku is only able to deploy 1 package (application) of this monorepo. That is, if you were to have a main client (React) and another sub-client (React), which both would be in their own individual packages, you'd have to pick which of the two you would like to deploy. What gets deployed is what's described in the Procfile
.
The contents of the Procfile
are:
web: node packages/server/build/src/index.js
That means when the Heroku app gets deployed, we are running on the web the node.js application, which is our "main" server. If we were to have another server, it probably wouldn't work, and you'd have to change what index.js
file should be chosen to be operated. So while you can have multiple packages that have an index.js
file, only one at a time can be chosen to be operated. One repo = one application, for Heroku. And in this setup, this monorepo is counted as a single repo.
Caveats: It's probably possible to run a database alongside this, but I haven't attempted to do so to add that in this boilerplate as my usecase doesn't require having a database included in the client-server monorepo.
Also on a side note, there may be a way to select a different index.js
file, or a Procfile
(assuming multiple of these) in Heroku with this buildpack: https://elements.heroku.com/buildpacks/lstoll/heroku-buildpack-monorepo
I haven't tried to configure Heroku with that buildpack, so I wouldn't know if it works or not, and if it's even viable to do so.
Naturally, if you aren't going to use Heroku, and will deploy this manually with other provides such as aws, then you can obviously choose to deploy whatever packages you wish.
I originally made this with the intention to work with TypeScript. As far as I'm concerned, there isn't any easy way to adjust this monorepo such that it'd work without TypeScript (vanilla JavaScript). I suppose you could just write normal JavaScript in the .ts
files as TypeScript is a superset of JavaScript. Or you can instead learn TypeScript, which I think is the better option since it comes with more features and makes code completion and refactoring easier.
The build script of the server is called automatically when the whole
monorepo's build script is called (via yarn build
in the root directory).
The command rm -rf build/
is called prior to compiling the TypeScript
files to clean the build/
folder of old files. This command may not work
on Windows.