28 October 2022 (updated: 7 November 2022) by
Paweł Sierant Paweł Sierant
The big guys are not wrong when they're using monorepos for their projects. Monorepos along with a generic template can make your project's setup faster (and cheaper). We're sharing our approach.
With the constant growth of complexity in applications comes a problem - how to maintain all the needs and reduce the costs of a project requiring that many dependencies from the beginning? In this article we’ll cover a way to save days of team’s work when starting a new project.
There is no one simple way to do that, but with years of experience and a search for the best practices from companies like Google, at EL Passion, we developed a solution that drastically reduces the amount of time needed to set up a new project.
In this article, we’ll cover the concepts of a template project, monorepos and our own approach on that topic.
There is a common misconception that keeping all code in one repository comes with all the disadvantages of monolith applications - it doesn’t scale, it forces us to release apps together… If yes, then why would companies like Google or Facebook use monorepos? The truth is actually quite the opposite. The reason why the one repository per project approach is so popular is because of the autonomy - teams can work in isolation, making decisions separately. The thing is - with proper tools monorepos can have all the benefits of separate repositories and even more.
Here are some of the features monorepo offers:
Code sharing - instead of creating separate repositories for shared code or even duplicating it on every repo, we can just share everything between our projects.
Consistency - all projects are built using the same tools
Visibility and refactoring - working with microservices, it’s easier to look at the code in one place than searching between multiple repositories
What's unique about our approach?
Using monorepos on its own can decrease the costs of development, but we went one step further and taking advantage of monorepos, we created a generic template for our upcoming projects.
Most apps’ development starts quite similarly. There is always a need to set up the architecture and basic configuration of the app. Even more application-specific modules like authorization often have some common logic.
With all this in mind, comes the idea of creating some sort of template that can be reused between different projects, saving days or weeks of the whole team’s work.
To prove that there is a significant difference in time between creating a new project and using a generic one I decided to give myself 2 days to set up a backend NestJS project from scratch.
At first, I took advantage of NestJS CLI and generated a starter project that includes some basic configurations. Then, even though I work with NestJS on a daily basis, it took me a few hours to set up database connection and simple environment variables validation. After another couple of hours I managed to run basic tests and create a Github Actions CI pipeline to run them automatically.
It took me some time to realize the mistakes I made, so I had to fix them. And here I was, after nearly 2 days struggling to make a basic app setup. So why was it like that?
Your project’s setup is usually done once a few months or even more. No matter how many years of experience or knowledge one may have, it’s impossible to remember all the small details that are necessary to create a functional commercial application.
Even if I did somehow remember everything that needed to be done, it would have taken forever to make it to the stage where it has all the features and helpers that we in EL Passion developed in our Flounder project.
As I mentioned before, most projects setup is similar - but not quite the same. Let’s say we have our starter monorepo project done… but do we really? Even though we have prepared an architecture and basic features, it’s not actually customizable. We don’t want to search through all the files and change the values of names, paths etc. every time we start a new project.
To solve that, we developed a solution that converts the project to a generic template by replacing every project-specific value with variables so then it can be easily customized for all the different projects.
You may wonder why we don't start with the variables from the beginning. Since the Flounder project is being constantly updated, we need it to be fully functional, so the developers can run and develop it as a normal project.
To automate stuff, we created a CI pipeline that first runs our script and then publishes the template to the Github packages registry, so it can be executed via command line, replacing all the variables with project-specific values or even skipping resources we don’t need for the specific project.
A step-by-step guide on how to create a monorepo generic template project
You should see a newly created project. Now, we want to install NestJS preset as a dev dependency in our project.
yarn add –dev @nrwl/nest
Once it’s installed, we can generate a new application.
nx generate @nrwl/nest:app backend
We will also create a node app as a lib
yarn add –dev @nrwl/node
nx generate @nrwl/node:lib utils
Nx has generated the whole configuration for us. We can now easily import anything from the utils lib to every other project. Below You can see how I imported the example method from utils into the app service in the NestJS application.
As you can see, our app is working perfectly fine and it’s ready for development. Nx handled everything for us and we can make use of all the advantages of monorepos.
Running the project
The next part is making the app generic, so it can be used as a starter for any other project. Let’s create a “template-setup” folder in our project’s main directory and a “revert-to-template.js” file inside it. We will also create a “config.js” file where we’ll store some helper values. The structure should look like this:
The project's folder structure
What we want to do now is to copy the project files and replace the values we want to be customizable with EJS variables. We also have to add “_” prefix to all files containing variables so they can be evaluated as EJS files.
Now if you run “node revert-to-template.js”, you should see a newly created folder with replaced file names and our variable in package.json. We won’t be running this command manually though, it should be handled in the Github Actions workflow, but we’ll cover it later.
With the template prepared, we’re gonna write a code that can utilize it and generate a project. Let’s start with creating a simple package.json file with two packages and a “bin” property so we can run it as an executable. Make sure to include your github profile or organization name in the “name” property as it will be used later in CI workflow to publish the code.
Finally, we’ll make use of the “commander” package to create our CLI program. It will take user provided params and generate a new project using the “scaffe” package.
We can now test the flow by manually running the files. First, run “node revert-to-template.js” just as before. Now change directory to the newly generated template, install the packages (“yarn install”) and type “node index.js ~/path-to-project -p ProjectName”. You should see a project with the replaced value of a “name” property in the package.json file.
The last thing we need to do is create a CI pipeline that publishes our template to npm.
If you’re not familiar with what’s going on in this snippet, I encourage you to read more on Github Actions docs. Essentially I used a “setup-node” action which creates a “.nprmc” file (more here: Publishing Node.js packages), ran our revert-to-template script and published it using auto generated GITHUB_TOKEN secret. Additionally I used an action to automatically bump the package.json version so there are no conflicts.
Now, if we push our code to the repository, we should see a running workflow and then our published package.
Github Actions’ workflow summary view
Let’s try to install it and see if everything works! Remember to login to the registry when installing private packages.
Setup and generation of a new template project
And that’s it! You should see a fully functional generated project with replaced package.json name value.
A sign of a good software development process is the constant pursuit of making things more flexible and scalable which directly translates into cost and time savings. One of the examples is our generic template project.
The example from this article featured only simple generated application with one variable being the app name. In our internal solution at EL Passion, we have prepared multiple applications (frontend and backend) with reusable modules and even a terraform configuration for AWS infrastructure saving days of work for multiple developers.