Feel free to read it in my personal blog tgmarinho.com
Intro
I replaced four (sdk, smart-contract, indexer-api, frontend app) GitHub projects with only one using Monorepo / TurboRepo.
I did a post about it. Check it out
The structure of my monorepo:
~/Developer/blog/monorepo (main*) » tree -L 3 --gitignore
.
├── README.md
├── apps
│ ├── frontend
│ │ ├── Procfile
│ │ ├── README.md
│ │ ├── __mocks__
│ │ ├── __tests__
│ │ ├── next-env.d.ts
│ │ ├── next.config.js
│ │ ├── package.json
│ │ ├── public
│ │ ├── src
│ ├── contract
│ │ ├── README.md
│ │ ├── contracts
│ │ ├── hardhat.config.ts
│ │ ├── package.json
│ │ ├── scripts
│ └── backend
│ ├── README.md
│ ├── Procfile
│ ├── build
│ ├── package.json
│ ├── src
│ ├── tsup.config.ts
├── package.json
├── packages
│ ├── contract-types
│ │ ├── README.md
│ │ ├── package.json
│ │ ├── src
│ │ └── tsconfig.json
│ ├── sdk
│ │ ├── README.md
│ │ ├── jest.config.js
│ │ ├── jest.setup.js
│ │ ├── package.json
│ │ ├── src
│ │ └── tsup.config.ts
│ ├── eslint-config-custom
│ │ ├── index.js
│ │ └── package.json
│ └── tsconfig
│ ├── README.md
│ ├── base.json
│ ├── nextjs.json
│ ├── package.json
│ └── react-library.json
├── turbo.json
└── yarn.lock
// Omitting some files and packages unnecessary to this blog post
A little bit of the context
The hard part was hosting, and I'll tell you how to do it on Heroku.
But before, I wish to give you some context on how this project should do in the build process:
The smart-contract should do the build process to generate all types using typechain lib, because instead of using the ABI, I want to use the types (typescript for the win); this build process generates the folder types inside of the
smart-contract project; and my script copies this types
folder and pastes it to new packages called contract-types (that should be an npm package of types).
The sdk should do the build process and use the contract-types, then the frontend app should build and use the sdk that uses the contract-types.
The indexer-api (backend) should build using the contract-types.
Order of the build:
- smart-contract
- the types of contract-types should exist
- sdk
- parallel frontend and backend
TurboRepo does it in a fast and intelligent way without much effort.
There are others things I'm omitting because they are not too important, but we have other packages.
With this in mind, let's see how to set up the Heroku to work on monorepo:
Deployment - Create the Apps
Create two apps on heroku:
- frontend -
heroku create -a frontend
- backend -
heroku create -a backend
Add Buildpacks
In both apps, you can connect Heroku apps to the Github. This way, you will save time with CI/CD after committing to the main branch.
Also, in both ones, you need to follow the same steps:
Add (GUI: settings -> buildpacks -> Add Buildpack) the buildpacks in this order:
Or Heroku CLI:
heroku buildpacks:add -a frontend heroku-community/multi-procfile
heroku buildpacks:add -a frontend heroku/nodejs
heroku buildpacks:add -a backend heroku-community/multi-procfile
heroku buildpacks:add -a backend heroku/nodejs
Create the Procfile
Procfile is a file that receives the commands to run when starting an application; if you have a basic node.js Heroku app, you don't need this once the package.json
has the start script instruction.
But in our case, we'll need this for frontend and backend packages:
Frontend:
echo "web: cd apps/frontend && yarn start" > Procfile
Backend:
echo "web: cd apps/backend && yarn start" > Procfile
The command above creates the Procfile file with the content: web: cd apps/backend && yarn start
Setup the new env PROCFILE for file path to Procfile:
Frontend App:
heroku config:set -a frontend PROCFILE=apps/frontend/Procfile
Backend App:
heroku config:set -a backend PROCFILE=apps/backend/Procfile
Setup the root package.json on monorepo
Heroku now knows where to find our Procfiles; however, because we have two separate applications stored within the frontend (client) and backend (server) directories, each has its dependencies.
Heroku typically tries to install dependencies as specified in the package.json at the project's root and will try to run the build script set here. To ensure we install the correct dependencies and run the proper build scripts for our application, we need to specify a heroku-postbuild script at the root of our project.
The secret ingredient of the recipe: In the package.json file in the project root, add the following scripts:
"build:frontend": "turbo run build --filter=frontend",
"build:backend": "turbo run build --filter=backend",
"heroku-postbuild": "if [ $CLIENT_ENV ]; then yarn run prod-frontend; elif [ $SERVER_ENV ]; then yarn run prod-backend; else echo no environment detected, please set CLIENT_ENV or SERVER_ENV; fi",
"prod-frontend": "yarn run build:frontend",
"prod-backend": "yarn run build:backend"
We’ve added three scripts: heroku-postbuild, prod-frontend, and prod-backend.
Heroku will automatically run the heroku-postbuild script for us upon deployment.
Our heroku-postbuild script looks for environment variables $CLIENT_ENV
or $SERVER_ENV
to determine which script to run prod-frontend
or prod-backend
.
Setting environment variables on Heroku
Now add new CLIENT_ENV and SERVER_ENV on heroku apps:
Frontend App:
heroku config:set -a frontend CLIENT_ENV=true
Backend App:
heroku config:set -a backend SERVER_ENV=true
Now our heroku-postbuild script will be able to run the correct install scripts for each of our applications on deployment.
See the package.json complete:
{
"name": "my-monorepo",
"version": "0.0.0",
"private": true,
"workspaces": [
"apps/*",
"packages/*"
],
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"dev:app": "turbo run dev --filter=frontend",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"build:app": "turbo run build --filter=frontend",
"build:api": "turbo run build --filter=backend",
"start:app": "turbo run start --filter=frontend",
"start:api": "turbo run start --filter=backend",
"heroku-postbuild": "if [ $CLIENT_ENV ]; then yarn run prod-frontend; elif [ $SERVER_ENV ]; then yarn run prod-backend; else echo no environment detected, please set CLIENT_ENV or SERVER_ENV; fi",
"prod-frontend": "yarn run build:app",
"prod-backend": "yarn run build:api"
},
"devDependencies": {
"eslint-config-custom": "latest",
"prettier": "latest",
"turbo": "latest",
"tsup": "^5.12.6"
},
"engines": {
"npm": ">=7.0.0",
"node": ">=8.0.0 <=16.14.2"
},
"dependencies": {},
"packageManager": "yarn@1.22.18",
}
🚨 I recommend not using the caches, but it's not a best practice; it's nice to study a better solution; I was facing issues keeping it true
:
heroku config:set USE_YARN_CACHE=false -a frontend
heroku config:set NODE_MODULES_CACHE=false -a frontend
heroku config:set YARN_PRODUCTION=false -a frontend
heroku config:set USE_YARN_CACHE=false -a backend
heroku config:set NODE_MODULES_CACHE=false -a backend
heroku config:set YARN_PRODUCTION=false -a backend
My turbo.json
:
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"start": {
"dependsOn": [
"^build"
]
},
"start:app": {
},
"lint": {
"outputs": []
},
"dev": {
"cache": false
}
}
}
Last but not least, you should run the deploy and see the result.
✅ Build and Deploy should pass. 🙏🏻
Conclusion
Excellent, you have a monorepo with turborepo running in production inside the Heroku.
Everything should now be proper to deploy multiple applications versioned under a monorepo to several Heroku applications.
Just set your Heroku applications up to deploy on push, and you should be ready to go next time you push any changes.
Always there is something to improve; what do I need to do? Github Actions, wait for the following chapters.
Finish ✌🏻
References:
Deploying a Monorepo to Heroku - by Sam
Pruning dependencies - Heroku Support NodeJS
__
Thanks for reading 🚀
Top comments (5)
Hey Thiago,
thanks for this article, it was a big help! I was wondering: you said, you're using pnpm. I assume you're using a custom buildpack on Heroku then? I'm using this one and it doesn't install dev dependencies on deploy (a sensible choice). As a result, I had to move turbo in dependecies.
Do you use any specifc setup with turbo/pnpm on top of what you described here? Maybe you want to share which buildpack you're using or some other useful tips?
Thanks!
Hey Andreas, sorry for delaying.
But when deploying an app to Heroku, it is important to consider dependencies and configuration. By default, Heroku installs only production dependencies, ignoring development dependencies under devDependencies. To make Heroku install devDependencies, set the npm production variable to false using
heroku config:set NPM_CONFIG_PRODUCTION=false
try this:
heroku config:set NPM_CONFIG_PRODUCTION=false
Oi, Thiago; Thank you so much for writing this article; it's awesome. I've followed along with everything you've outlined here, but encountered some issues. Specifically, upon deployment to Heroku, the build fails with the issue
unknown flag: --filter=backend
. Did you encounter this sort of trouble and if so, how did you resolve it?Many thanks & muito obrigado,
Alex
Thanks Alex, I'm using this version: "turbo": "1.6.3" and you? Also using pnpm: pnpm@7.14.2
Do you have this project on Github?
Hi Thiago,
Thanks for this article, very helpful. I followed the steps you outlined here, but running into an issue and wondering if you came across the same thing.
My repo structure looks like this:
I am only deploying my server to Heroku, as I am using Vercel for the frontend. Worth noting as well that I am using pnpm. The buildpacks I am using are as follows (loaded in this order):
1.) github.com/heroku/heroku-buildpack...
2.) github.com/unfold/heroku-buildpack...
3.) github.com/heroku/heroku-buildpack...
I've also enabled the following environment variables:
The Procfile looks like this:
My package.json looks like this:
And turbo, looks like this:
When I push to Heroku, everything seems to work as it should, all the way until the end – this is the full output. One thing that is strange is it looks like it's building twice (?). If you have any ideas, they would be greatly appreciated – I have tried a bunch of different things, and none the wiser. Thank you in advance!