This is Part 1 of a series on plan-based development with Claude Code. We’ll explore how combining planning documents, TypeScript, Turborepo, and AI assistance creates a development workflow that’s faster and more reliable.
The Challenge of Modern Monorepo Development
Picture this: You’re managing a TypeScript monorepo with over 60 packages. There are multiple apps (web, server, mobile), dozens of shared packages (UI components, API clients, AI integrations, database layers), and a team that needs to move fast without breaking things.
This is our reality today. But it didn’t start this way.
From Single App to Sprawling Monorepo
Six months ago, we had a single Next.js application with everything in one place. Authentication logic lived next to UI components. Database queries shared files with API routes. It worked—until it didn’t.
The inflection points came quickly:
- Adding a mobile app: We needed the same authentication and API logic for React Native. Copy-paste wasn’t going to cut it.
- Growing the team: Multiple developers working in the same directories meant constant merge conflicts.
- Performance requirements: Build times stretched to 10+ minutes as the app grew.
- AI integration explosion: What started as one LLM call became a dozen specialized AI tools, each with their own prompts, schemas, and test data.
So we started extracting. First @bts/auth for shared authentication. Then @bts/db for database logic. Then UI components, then API routes, then AI tools. Before we knew it, we had 60+ packages.
When to Extract: Our Decision Framework
Not everything belongs in a package. Here’s the framework we use:
Extract when:
- Shared across apps: If both web and mobile need it, it’s a package
- Independent testing valuable: Complex logic that benefits from isolated unit tests
- Clear API boundary: You can define what goes in and what comes out
- Stable interface: The contract between this code and its consumers is well-understood
- Build optimization: Isolating code allows Turborepo to cache it separately
Keep inline when:
- App-specific: Only one app uses it and always will
- Rapidly evolving: The interface changes with every PR
- Tightly coupled: It can’t be understood without its surrounding context
- Trivial: A few utility functions don’t need their own package
The rule of three: When you copy code to a third location, it’s time to extract. Two copies might be coincidence. Three copies is a pattern.
The Package Naming Convention
We adopted a naming pattern that makes packages discoverable:
@bts/[domain]-[type]
Examples:@bts/project-router # Project domain, router type@bts/project-ui # Project domain, UI components@bts/project-ai # Project domain, AI tools@bts/core-ui # Shared/core UI components@bts/core-ai # Shared AI utilitiesThis convention means you can predict where code lives. Need the survey API routes? Check @bts/survey-router. Need survey-related AI tools? Check @bts/survey-ai.
The Challenge of Scale
The traditional approach of “write code, test manually, fix bugs, repeat” simply doesn’t scale at this complexity level. A change to @bts/db can affect 30 downstream packages. A type change in @bts/schemas ripples through the entire system.
We needed tooling and practices that could handle this complexity without slowing us down.
What we discovered was that combining plan-based development, aggressive type checking, intelligent build orchestration, and AI-assisted coding creates a feedback loop so tight that we can confidently make sweeping changes across the entire codebase in hours instead of days.
Next up in Part 2: We’ll dive deep into plan-based development and how writing the plan before the code changes everything.