a Senior Fullstack Developer atVazco
Every project begins with a simple idea and limited resources. Our startup was no exception. We are a team of 3 developers creating a mobile app for rugby clubs. Initially, we worked on the backend and mobile application in two separate repositories, connecting them with REST API. However, over time, it became clear that we needed to optimize the app.
Switching to GraphQL promised to solve many issues with data exchange between the backend and mobile app. To achieve this, we rewrote our backend from Java to Node.js (it doesn’t matter in this article's context), adapted the mobile app to the new API, and kept the two repositories separate.
This worked fine at first, but working with two repositories started creating unnecessary complications over time. Managing two separate PRs for every new feature became a real pain as we added more features. It seemed trivial at first, but after doing it a hundred times, it started annoying us. Sometimes, the backend PR would merge before the front end or vice versa, and we’d have mismatched features and inconsistent functionality. This confused team members, delayed testing, and even caused bugs in production. It was clear we needed to change our workflow.
Another issue was the process of generating GraphQL types (we use @graphql-codegen/cli
). The mobile app required an up-to-date backend schema for introspection, forcing us to deploy the backend before building the mobile bundle. This added at least 10–15 minutes to each release.
We realized something needed to be changed. Fortunately, the backend was already partially set up for a monorepo using Nx, but without active caching or configured tasks. Instead of using Nx on full power, we ran commands directly from the workspace package npm run start -w backend
. However, Nx felt overly complex for our needs, so we switched to a relatively new tool called Turborepo. It looked like we can make the process simpler and more efficient.
Turborepo is a high-performance build system for JS and TS codebases. It is designed for scaling monorepos and also makes workflows in single-package workspaces faster, too.
The decision was made: merge the backend and mobile app into a monorepo. This transition was challenging but opened up many new opportunities, which I will share later.
The process was far from straightforward. My first attempt was directly merging the backend repository into the mobile app repository. At first glance, it seemed like a reasonable approach, but it quickly became evident that this method introduced more problems than solved.
Initial problems
graphql
, jest
, etc. I was obligated to bump packages to the latest version to fix the issue. In our case, it was pretty straightforward. node_modules
. Yarn and pnpm package managers allow you to do that, but not npm yet (even though workspaces support was introduced since version 7).After facing these issues, I realized that migrating everything at once was overly ambitious. I decided to start fresh and take a step-by-step approach:
Also, remember to preserve the commit history while merging repositories. If it’s your side project, don’t worry too much about it. But if you’re working with a team, keeping a history of file changes is crucial (because everyone will blame you for writing bad code even though it’s not yours!). Here is an excellent explanation of how to merge them properly.
Once the repositories were combined, I archived the old backend repository and updated all workflows to align with the monorepo structure.
One of the most notable improvements was the ability to perform GraphQL introspection locally using a schema file instead of deploying the backend.
Our previous process was this: after changing the GraphQL schema, we deployed a new version of the backend (deployment lasts for ~15 minutes), then we manually triggered the build app bundle workflow that introspected the backend GraphQL schema and then did the rest of the job. There was no way to do it faster because the schema file was in the other repository.
That’s why migration to monorepo eliminated the 10–15 minutes previously spent deploying the backend before building the mobile app. It also allowed us to decouple app releases from backend deployments, significantly improving our efficiency.
Now, we have configured codegen as a root Turborepo task, and we can easily share the GraphQL schema between the backend and mobile packages.
Github has a limit of 10GB for cache entries by default for every repository. We use ~8GB because of caching node_modules
, cocoapods, and maven packages for optimizing mobile build workflow. You can easily reach this limit with more packages in the monorepo. After reaching it, you will have a few options: skip the caching step and cache only the most essential things, or I don’t know even, good luck.
Ultimately, your comfort and workflow preferences should guide your decision. What matters is how comfortable you feel developing a product. Please, don’t overcomplicate things for yourself. If you want to try new approaches, go ahead. The worst thing that can happen is that you will gain new experience and knowledge.
But for pedants (detail-oriented individuals) or teams with big projects, I’d recommend thinking twice before making the migration. It depends on your specific needs. There are not so many technical reasons to do this, but it can still improve your overall DX.
Remember to configure caching correctly for your workflows and make sure your workflows are OS-agnostic. In my case, I forgot to specify the key for actions/cache@v4
based on the machine’s OS, and we had a problem with building the mobile app for iOS and Android. Sometimes, it worked for iOS but not for Android, and sometimes vice-versa. During the workflow, npm
installed packages for Linux (x86_64 arch) and cached them, and it triggered an error during iOS builds.
Debugging it was a nightmare since I had to wait half an hour for each platform to be built each time to check if the issue was resolved.
- name: Cache node_modules
uses: actions/cache@v4
id: cache-npm
with:
path: |
./node_modules
./apps/mobile/node_modules
./apps/backend/node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('package-lock.json') }}