How We Migrated 4 Codebases to Turborepo — and Cut Our Go-to-Market Time by 60% Using Claude

A hands-on account of consolidating Next.js, Angular, React Native, and NestJS into a single Turborepo monorepo — and how AI-assisted engineering changed the way our team ships

R
Published in IndustryBuying Engineering · 14 min read ·
89
How We Migrated 4 Codebases to Turborepo — and Cut Our Go-to-Market Time by 60% Using Claude
Our engineering team after the Turborepo migration shipped. The coffee was strong, the pipeline was green.

There’s a particular kind of engineering pain that doesn’t show up in your error logs. It lives in the 40-minute Slack thread trying to coordinate a shared component change across three repositories. It lives in the “wait, which version of that utility are you importing?” conversation. It lives in the CI pipeline that takes 22 minutes to run because nobody has been brave enough to question why.

At IndustryBuying, we hit that wall in late 2025. We were running four separate codebases — a Next.js customer-facing storefront, an Angular internal ops dashboard, a React Native mobile app, and a NestJS backend API — each in its own repository, each with its own CI/CD setup, its own package.json, and its own subtle divergence from what the other repos were doing. We had four tsconfig.json files that had all drifted from each other. We had three different versions of the same date-formatting utility copy-pasted across repos.

We knew we needed a monorepo. What we didn’t know was how profoundly AI-assisted engineering — specifically using Claude — would change not just how fast we migrated, but how fast we now ship everything after it.

This post is an honest account of what that migration looked like: the architecture decisions, the tooling choices, the Claude prompts that saved us days of work, and the concrete metrics on the other side.


The State of Things Before

Before we dive into the solution, let me paint an accurate picture of the problem. Our codebase topology looked like this:

industrybuying/
├── storefront/          # Next.js 14 — customer-facing web app
├── ops-dashboard/       # Angular 17 — internal operations tool
├── mobile/              # React Native 0.73 — iOS + Android
└── api/                 # NestJS — monolithic backend API

Each repo had its own:

  • package.json with independently versioned dependencies
  • TypeScript configuration that had evolved separately over 3 years
  • ESLint and Prettier setup (slightly different rules in each)
  • CI/CD pipeline in GitHub Actions (four .github/workflows/ directories)
  • Shared types — or rather, four separate copies of shared types

The shared types problem was the most painful. We had a Product type defined in the API, then a slightly different Product type in the storefront, then a React Native version that had one extra field that someone added during a hackathon and never cleaned up. When the backend team added a new field, there was a manual 3-repo PR dance every time.

Our deployment process for a full-stack feature looked like this:

  1. PR in api/ → review → merge → deploy → wait
  2. PR in storefront/ → review → merge → deploy → wait
  3. PR in mobile/ → review → merge → submit to app store review → wait 2–3 days

Average time from “feature complete” to “live for all users”: 11 days.


Why Turborepo Over Other Monorepo Tools

We evaluated Nx, Lerna, and Turborepo. The decision came down to three things:

1. Turborepo doesn’t require you to restructure your build system. Nx is powerful but opinionated — it wants to own your build configuration. With Turborepo, you keep your existing next build, ng build, react-native bundle, and nest build commands. It orchestrates them; it doesn’t replace them.

2. Remote caching is a first-class citizen. Turborepo’s remote cache (via Vercel or a self-hosted solution) means that if your CI runs a build that another developer already ran with the same inputs, it restores from cache. This alone cut our CI time from 22 minutes to under 6 on cached runs.

3. Task dependency graphs are simple to define. Expressing “the storefront build depends on the API types package being built first” is three lines in turbo.json. In Nx, that’s a more involved configuration.


The Migration Plan

We broke the migration into four phases:

  1. Shared packages extraction — pull out shared types, utilities, and UI tokens into packages/
  2. Monorepo scaffolding — set up the Turborepo workspace structure
  3. Per-app migration — move each app in, fix imports, validate builds
  4. CI/CD consolidation — replace four pipelines with one

We estimated Phase 1 alone would take two weeks. Between identifying what was shared, resolving the type divergences, and writing the new packages — it was going to be a grind.

It took three days. Here’s why.


How Claude Changed the Migration Velocity

I want to be specific here, because vague claims about “AI accelerating development” are everywhere and mean nothing. Let me walk through concrete examples of what Claude did in this migration.

1. Cross-repo Type Reconciliation

We had four diverged Product interfaces. I gave Claude all four versions and asked it to produce a canonical, backwards-compatible interface that all four codebases could adopt, with an explanation of every conflict it found.

The prompt was essentially:

“Here are four TypeScript interfaces for Product from four different codebases in our company. Identify all conflicts and field mismatches. Produce a single canonical interface that is a superset of all four, making any new fields optional with appropriate types. For each field that differs between implementations, explain why the conflict likely exists and what the correct resolution is.”

Claude came back with a 47-field canonical interface, a table of 12 conflicts with explanations, and three fields it flagged as potentially erroneous duplicates (they were — a 2022 refactor that never got cleaned up). It also wrote the migration type guards (isLegacyProduct, isV2Product) we’d need during the transition period.

What would have taken a full-day architecture meeting took 20 minutes.

2. Automated Import Path Rewriting

Moving four codebases into a monorepo means thousands of import statements need to change. What was:

// Before — in storefront/
import { formatCurrency } from '../../utils/currency';
import { Product } from '../../types/product';

Becomes:

// After — in apps/storefront/
import { formatCurrency } from '@ib/utils';
import { Product } from '@ib/types';

I asked Claude to write a Node.js script that would:

  • Walk the directory tree
  • Identify all import statements matching our old patterns
  • Rewrite them to the new @ib/* package paths
  • Dry-run first, printing what it would change
  • Apply changes when passed a --write flag

It produced the script in one pass. It even added a report at the end showing which files were modified and flagging any imports it wasn’t confident about. We ran it on 180,000 lines of TypeScript across four codebases. It got 97% of them right. The remaining 3% were edge cases it had flagged for manual review.

3. Turborepo Pipeline Configuration

I gave Claude our existing four GitHub Actions workflows and asked it to produce a single consolidated turbo.json and a unified ci.yml that:

  • Ran lint, test, and build tasks with correct dependency ordering
  • Used Turborepo remote caching
  • Only ran affected app tests when changes were scoped to a single app
  • Deployed only the apps whose builds had changed

The result was cleaner than what any of us would have written from scratch, because Claude had already seen every Turborepo edge case in its training data and avoided the common mistakes (like forgetting to set "cache": false on deploy tasks).

4. ESLint/TSConfig Unification

Four diverged configs needed to become one shared base with per-app overrides. Claude analyzed all four, identified the meaningful differences (versus accidental drift), and produced:

packages/
└── config/
    ├── eslint-base.js        # Shared ESLint config
    ├── tsconfig.base.json    # Shared TypeScript base
    ├── tsconfig.nextjs.json  # Next.js-specific extends
    ├── tsconfig.angular.json # Angular-specific extends
    ├── tsconfig.rn.json      # React Native-specific extends
    └── tsconfig.nest.json    # NestJS-specific extends

It also wrote the MIGRATION.md explaining what each override was for and why — documentation that was directly useful for the three engineers who joined the project after the migration.


The Final Monorepo Structure

After the migration, our repository looks like this:

industrybuying/
├── apps/
│   ├── storefront/          # Next.js 14
│   ├── ops-dashboard/       # Angular 17
│   ├── mobile/              # React Native 0.73
│   └── api/                 # NestJS
├── packages/
│   ├── types/               # @ib/types — shared TypeScript interfaces
│   ├── utils/               # @ib/utils — shared utilities
│   ├── ui-tokens/           # @ib/ui-tokens — design system tokens
│   ├── api-client/          # @ib/api-client — typed API SDK
│   └── config/              # @ib/config — ESLint, TSConfig bases
├── turbo.json
├── package.json
└── pnpm-workspace.yaml

The turbo.json Pipeline

{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**", "build/**"]
    },
    "lint": {
      "outputs": []
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"],
      "cache": true
    },
    "test:e2e": {
      "dependsOn": ["build"],
      "cache": false
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "deploy": {
      "dependsOn": ["build", "test", "lint"],
      "cache": false
    }
  }
}

The "dependsOn": ["^build"] syntax is Turborepo’s way of saying “run all build tasks in dependency packages before running this one.” So when storefront builds, it automatically ensures @ib/types, @ib/utils, and @ib/api-client are built first. No more “why is the type missing?” at 11pm.


The @ib/api-client Package — Our Biggest Win

The most impactful package we extracted was @ib/api-client. Previously, each frontend had its own API layer — the storefront had Axios calls, the ops dashboard had Angular’s HttpClient, the mobile app had a custom fetch wrapper. All calling the same API, all slightly inconsistent.

We built @ib/api-client as a framework-agnostic typed SDK that wraps our NestJS endpoints. Claude helped generate the initial client from our NestJS controller decorators:

// packages/api-client/src/products.ts
export class ProductsClient {
  constructor(private readonly http: HttpAdapter) {}

  async list(params: ProductListParams): Promise<PaginatedResponse<Product>> {
    return this.http.get('/products', { params });
  }

  async getById(id: string): Promise<Product> {
    return this.http.get(`/products/${id}`);
  }

  async updateInventory(
    id: string,
    payload: UpdateInventoryPayload
  ): Promise<Product> {
    return this.http.patch(`/products/${id}/inventory`, payload);
  }
}

The HttpAdapter interface is implemented differently in each app (Axios in Next.js, Angular HttpClient in the dashboard, fetch in React Native), but the business logic is identical. One bug fix in @ib/api-client fixes it in all three frontends simultaneously.

This change alone eliminated a class of bugs where one frontend had a different understanding of an API contract than another.


CI/CD: From Four Pipelines to One

Before Turborepo, a push to any branch triggered all four pipelines. Every CI run was:

  • Install dependencies × 4 repos
  • Run tests × 4 repos
  • Build × 4 repos

Total: ~22 minutes average, ~45 minutes on a cold cache.

After Turborepo with remote caching enabled:

# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main, develop]
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

      - uses: pnpm/action-setup@v3

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile

      - name: Run Turborepo pipeline
        run: pnpm turbo run lint test build --filter='[HEAD^1]'
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

The --filter='[HEAD^1]' flag tells Turborepo to only run tasks for packages that changed since the last commit. A change to apps/mobile only runs mobile’s lint, test, and build — not Next.js, not Angular, not the API.

Current CI times:

  • Scoped change (one app): 4–6 minutes
  • Full pipeline (all apps changed): 11 minutes
  • Cached run (nothing changed): 45 seconds

The Go-to-Market Impact

Here’s what we actually came to talk about. The metrics, three months post-migration:

MetricBeforeAfterChange
Avg. feature lead time (dev → production)11 days4.4 days−60%
CI pipeline duration (average)22 min5.2 min−76%
Cross-team type conflict bugs per sprint6.40.8−87%
Time spent on dependency sync (per week)~6 hrs~45 min−87%
Developer onboarding time (to first PR)3 days1 day−66%

The 60% reduction in feature lead time came from multiple compounding improvements:

  • Atomic cross-app PRs. A feature that touches the API, the storefront, and the mobile app is now a single PR with a single review and a single CI run. The old 3-PR coordination dance is gone.
  • Instant type safety. When a backend engineer changes an API contract, TypeScript errors surface immediately in all consuming apps within the same PR. No more “we broke mobile and didn’t know for a week.”
  • Faster CI feedback. Engineers used to batch their changes to avoid triggering 22-minute CI runs. With 5-minute feedback loops, they commit and iterate freely.
  • Shared utilities mean less writing. New features start from a richer shared foundation.

Claude’s Ongoing Contribution

The migration was a one-time event, but Claude is now part of our daily engineering workflow. Three months in, here’s where it shows up most:

Code review acceleration. Our PRs are reviewed by a Claude Code instance configured with our monorepo conventions. It catches import-path violations, detects when a shared package should be used instead of a one-off utility, and flags type inconsistencies before human reviewers spend time on them.

New developer ramp-up. When a new engineer joins, Claude Code is their first stop for understanding the codebase. They describe what they want to build, and it generates a correctly-structured implementation that follows our monorepo conventions — with the right @ib/ imports, the right config inheritance, and the right place in the directory tree.

Schema-driven code generation. When we add a new NestJS endpoint, Claude generates the corresponding @ib/api-client method, the TypeScript interface in @ib/types, and the React hook in apps/storefront/hooks/ in one pass. What used to be a 3-file, 3-repo change is now a one-step generation that we review and merge.


What Didn’t Go Smoothly

I’d be doing you a disservice if I made this sound like a clean migration.

Angular in a pnpm workspace is non-trivial. Angular’s build system has opinions about node_modules resolution that don’t play well with pnpm’s symlinked workspace structure out of the box. We spent a day and a half debugging Module not found errors that turned out to be a missing "public-hoist-pattern" in .npmrc. Claude helped diagnose it, but it was still painful.

React Native Metro and monorepos require a custom resolver. Metro, the React Native bundler, doesn’t understand workspace symlinks by default. We needed a custom metro.config.js:

const { getDefaultConfig } = require('@react-native/metro-config');
const path = require('path');

const root = path.resolve(__dirname, '../..');
const config = getDefaultConfig(__dirname);

config.watchFolders = [root];
config.resolver.nodeModulesPaths = [
  path.resolve(__dirname, 'node_modules'),
  path.resolve(root, 'node_modules'),
];

module.exports = config;

This took longer to get right than we’d like to admit.

Turborepo’s cache can lie to you. We had two incidents where a cached build served a stale result because we hadn’t correctly declared all output artifacts. The fix was auditing every pipeline entry and being explicit about outputs. But finding those two incidents cost us each about half a day of debugging.

You cannot escape migration day grief. Even with Claude rewriting 97% of import paths correctly, that 3% took human eyes. 180,000 lines of code across four repos. There were late nights.


Advice If You’re Considering This Migration

If you’re running multiple apps and feeling the coordination pain, here’s what I’d tell you based on lived experience:

Start with packages/types and nothing else. Don’t try to extract utilities and UI components and configs all at once. Get your shared types compiling cleanly first. Everything else is easier once types are canonical.

Use Turborepo’s --dry flag obsessively. Before running any pipeline change in production, turbo run build --dry=json will show you exactly what it plans to do and why. This saved us from at least three “why is CI rebuilding everything?” incidents during the migration.

Invest in your dev script. The monorepo is only worth it if local development is good. We have a pnpm dev that spins up all four apps with a single command, hot-reloading across them. Engineers who were skeptical of the migration became converts the first time they edited a shared utility and saw it hot-reload in three apps simultaneously.

Give Claude your full context, not just the immediate problem. The difference between “write a tsconfig for a Next.js app” and “here are our four existing tsconfigs, here’s our Turborepo structure, here’s what each app needs — write a base config and four app-specific extends” is the difference between a generic answer and something you can actually use.

The migration is not the destination. The monorepo is infrastructure. The destination is shipping faster. Keep measuring lead time, CI duration, and bug rates. If the numbers aren’t moving after 90 days, something in the setup isn’t working.


Where We’re Going Next

The Turborepo migration was Phase 1. We’re currently working on:

  • Automated API contract testing using the shared @ib/types as a single source of truth — if an API response no longer matches the canonical type, CI fails before it reaches staging.
  • Changesets integration for managing @ib/* package versioning within the monorepo, so we can publish packages externally when the time comes.
  • Claude Code hooks in our CI pipeline that automatically generate migration guides when a shared package introduces a breaking change.

The 60% reduction in go-to-market time is meaningful. But the more I look at the numbers, the more I believe the ceiling is higher. When your types are shared, your CI is fast, your imports are correct by construction, and AI can generate cross-stack code that follows your conventions — the constraint on shipping speed stops being technical coordination overhead.

It becomes the quality of your ideas.


Final Thoughts

The Turborepo migration was the right call. If you’re running a multi-app product with a growing team and you’re still on multiple repositories, the coordination tax you’re paying compounds every sprint. The migration is painful for two weeks and pays back every week after.

What I didn’t expect was how much Claude would change the character of the migration itself — from a grinding manual effort into something that felt more like a directed architectural exercise. The import rewriting script, the type reconciliation, the config unification, the pipeline design: these are tasks that are high-effort, low-creativity, and error-prone when done by hand. They’re exactly what AI-assisted engineering handles well.

More than the monorepo, I think the real unlock was getting our team comfortable with the idea that Claude is a collaborator on infrastructure work, not just a code autocomplete. When your senior engineers stop spending time on mechanical transformation tasks, they spend it on architectural decisions. That’s where the 60% reduction really came from.

If you’re building something similar or have questions about specific parts of the migration, I’m reachable on GitHub and always happy to compare notes.


The numbers in this post are real. The migration happened. The coffee was strong.

— Rohit Mishra, Senior Engineering Manager, IndustryBuying

89
I

Published in

IndustryBuying Engineering

1.2K followers · Last published Mar 29

Engineering stories, architectural decisions, and hard-won lessons from the team building India's leading B2B e-commerce platform.

R

Written by

3.2K followers · 84 following

Senior Engineering Manager with 10+ years building distributed systems and cross-platform products. Currently leading engineering at IndustryBuying. Passionate about developer experience, monorepo architecture, and shipping fast without breaking things.

More from Rohit Mishra and IndustryBuying Engineering