DEV Community

Cover image for Scaling AI-Assisted Development: How Scaffolding Solved My Monorepo Chaos
Vuong Ngo
Vuong Ngo

Posted on

Scaling AI-Assisted Development: How Scaffolding Solved My Monorepo Chaos

The Moment I Realized AI Coding Was Broken.

It was 10PM. I'd just asked Claude to add a navigation component. Thirty seconds later, I was staring at this:

// What the AI generated (again)
const Navigation = ({ items }: NavigationProps) => {
  const [open, setOpen] = useState(false);
  return <nav className="navigation">...</nav>
}
export default Navigation;
Enter fullscreen mode Exit fullscreen mode

Nothing wrong with it, technically. Except I don't use default exports. I use named exports. And useState should come from our custom hooks. And we use 'isOpen', not 'open'. And the TypeScript interface should be exported separately like every other component in our codebase.

I'd explained this exact pattern so many times I'd lost count.

Same pattern. Different day. Different component. Different wrong implementation.

This wasn't a one-off. My monorepo had become a Frankenstein's monster of inconsistent patterns—each one technically correct, all of them a maintenance nightmare.

The promise was simple: AI would code faster than humans.

The reality? I was spending more time fixing AI-generated code than I would've spent just writing it myself.

How AI-Assisted Development Actually Breaks

Here's what nobody tells you about scaling with AI coding assistants:

Week 1: The Honeymoon Phase

You: "Build me a login page"
AI: ✨ generates perfect login page
You: "Holy shit, this is the future"

Everything works. You're shipping features at 10x speed. Your manager thinks you're a wizard. You're thinking about that promotion.

Month 1: The Cracks Appear

You're reviewing frontend components and notice something odd:

// TaskBadge.tsx (written 2 weeks ago)
export const TaskBadge = ({ status }: TaskBadgeProps) => {
  return <span className={`badge ${getColor(status)}`}>{status}</span>;
};

// PriorityBadge.tsx (written yesterday)
export function PriorityBadge(props: PriorityProps) {
  const color = getPriorityColor(props.priority);
  return <div className={color}>{props.priority}</div>;
}

// StatusLabel.tsx (written today)
function StatusLabel({ value }: StatusProps) {
  return <Badge variant={getVariant(value)}>{value}</Badge>;
}
Enter fullscreen mode Exit fullscreen mode

Three badge components. Three different patterns. All from the same AI. All from the same human (you).

And on the backend, it's the same story:

// userService.ts (2 weeks ago)
@injectable()
export class UserService {
  constructor(@inject(TYPES.Database) private db: Database) {}
}

// authService.ts (yesterday)
export class AuthService {
  private db: Database;
  constructor(database: Database) {
    this.db = database;
  }
}

// paymentService.ts (today)
class PaymentService {
  constructor(public database: Database) {}
}
Enter fullscreen mode Exit fullscreen mode

Three services. One uses dependency injection properly. One doesn't. One is halfway there.

"Okay, I need better instructions," you think.

Month 2: The Documentation Death Spiral

Your CLAUDE.md file has grown massively. You've documented:

  • Component patterns ✓
  • Import styles ✓
  • File naming ✓
  • Prop validation ✓
  • Error handling ✓
  • State management ✓
  • API patterns ✓

You've told the AI everything.

Then you ask it to create a settings page, and it still uses a different button component than the rest of your app.

"But I literally documented this!" you scream at your screen at 3 AM.

The AI apologizes (One-time, I said "Your're f*king right" which is hilarious). Generates a new version. Wrong again, but differently wrong.

Month 3: The Breaking Point

You're now maintaining:

  • Dozens of CLAUDE.md files scattered everywhere
  • Multiple variations of what should be the same pattern
  • A massive style guide that the AI follows inconsistently
  • Code reviews that are mostly style debates instead of logic discussions

The math breaks: You're spending hours fixing what should've taken minutes to write.

This was me. And this was my monorepo:

  • Frontend apps built with Next.js and TanStack Start
  • Backend APIs using Hono.js, FastAPI and Lambda
  • Shared packages for everything reusable
  • Microservices, edge functions, and infrastructure all in one repo

The bigger it grew, the worse it got. And I wasn't alone.

My Failed Experiments

Attempt 1: The Mega CLAUDE.md

I created comprehensive documentation files referencing everything:

  • Project Structure
  • Coding Standards
  • Technology Stack
  • Conventions
  • Style System
  • Development Process

Result: Even with token-efficient docs, I couldn't cover all design patterns across multiple languages and frameworks. AI still made mistakes.

Attempt 2: CLAUDE.md Everywhere

"Maybe collocated instructions work better?" I created CLAUDE.md files everywhere for different apps, APIs, and packages.

Result: Slightly better when loaded in context (which didn't always happen). But the real issue: I only had a handful of distinct patterns. Maintaining dozens of instruction files for those same patterns? Nightmare fuel.

Attempt 3: Autonomous Workflows

I set up autonomous loops: PRD → code → lint/test → fix → repeat.

Result: I spent more time removing code and fixing bugs than if I'd just coded it myself. The AI would hallucinate solutions, ignore patterns, and create technical debt faster than I could clean it up.

The Three Core Problems

1. Inconsistency Across Codebase

Frontend:

// AgentStatus.tsx - uses our design system
export const AgentStatus = ({ status }: Props) => {
  return <Badge className={getStatusColor(status)}>{status}</Badge>;
};

// TaskStatus.tsx - reinvents the wheel
export function TaskStatus({ task }: TaskProps) {
  return <div className="status-badge">{task.status}</div>;
}

// SessionStatus.tsx - different again
const SessionStatus = (props: SessionProps) => (
  <span className={styles.badge}>{props.status}</span>
);
Enter fullscreen mode Exit fullscreen mode

Backend:

// taskRepo.ts - proper DI
@injectable()
export class TaskRepository {
  constructor(@inject(TYPES.Database) private db: IDatabaseService) {}
}

// projectRepo.ts - missing decorator
export class ProjectRepository {
  constructor(private db: IDatabaseService) {}
}

// memberRepo.ts - no DI at all
export class MemberRepository {
  private db = getDatabaseClient();
}
Enter fullscreen mode Exit fullscreen mode

Same concept, different implementations. All technically correct. All maintenance nightmares.

2. Context Window Overload

Your documentation grows from this:

# Conventions
- Use functional components
- Use TypeScript
Enter fullscreen mode Exit fullscreen mode

To this monstrosity:

# Conventions
## Components
- Use functional components
- Props interface must be exported
- Use PascalCase for component names
...(10+ reference docs)
Enter fullscreen mode Exit fullscreen mode

Eventually, even AI can't keep up.

3. Pattern Recreation Waste

How many times have you watched AI recreate the same pattern?

  • Authenticated API routes with similar structure
  • Badge components that look identical but use different approaches
  • Repository classes with the same DI pattern but inconsistent implementation
  • Service classes that all need the same base configuration

Each time slightly different. Hours wasted on work already done.

The Solution: Intelligent Scaffolding

Instead of fighting these problems with longer instructions, I needed a fundamental shift: teach AI to use templates, not just write code.
How It Works: The scaffolding approach leverages MCP (Model Context Protocol) to expose template generation as a tool that AI agents can call. It uses structured output (JSON Schema validation) for the initial code generation, ensuring variables are properly typed and validated. This generated code then serves as guided generation for the LLM—providing a solid foundation that follows your patterns, which the AI can then enhance with context-specific logic. Think of it as "fill-in-the-blanks" coding: the structure is guaranteed consistent, while the AI adds intelligence where it matters.

The Key Insight

Traditional scaffolding requires complete, rigid templates. But with AI coding assistants, you only need:

  1. A skeleton with minimal code
  2. A header comment declaring the pattern and rules
  3. Let the AI fill in the blanks contextually

Example from our actual codebase:

/**
 * PATTERN: Injectable Service with Dependency Injection
 * - MUST use @injectable() decorator
 * - MUST inject dependencies with @inject(TYPES.*)
 * - MUST define constructor parameters as private/public based on usage
 * - MUST include JSDoc with design principles
 */
@injectable()
export class {{ ServiceName }}Service {
  constructor(
    @inject(TYPES.Database) private db: IDatabaseService,
    @inject(TYPES.Config) private config: Config,
  ) {
    // AI fills in initialization logic
  }

  // AI generates methods following the established pattern
}
Enter fullscreen mode Exit fullscreen mode

The AI now knows the rules and generates code that follows them.

Enter: Scaffold MCP

I built the @agiflowai/scaffold-mcp to implement this approach. It's an MCP (Model Context Protocol) server that provides:

  1. Boilerplate templates for new projects
  2. Feature scaffolds for adding to existing projects
  3. AI-friendly minimal templates with clear patterns

Why MCP?

  • ✅ Works with Claude Desktop, Cursor, or any MCP-compatible tool
  • ✅ Tech stack agnostic (Next.js, React, Hono.js, your custom setup)
  • ✅ Multiple modes: MCP server or standalone CLI
  • ✅ Always available to AI like any other tool

Real-World Workflow Transformation

Before Scaffolding: Starting a New API

You: "Create a new Hono API with authentication"
AI: *generates files with different patterns*
You: "Wait, where's the dependency injection?"
You: "Can you use our standard middleware setup?"
You: "Actually, use Zod for validation like our other APIs..."
*Back-and-forth debugging*
Enter fullscreen mode Exit fullscreen mode

After Scaffolding: Starting a New API

Using CLI:

# See available templates
scaffold-mcp boilerplate list

# Create API with exact conventions
scaffold-mcp boilerplate create hono-api-boilerplate \
  --vars '{"apiName":"notification-api","port":"3002"}'

# ✓ Complete API structure created
# ✓ Dependency injection configured
# ✓ All following your team's conventions
Enter fullscreen mode Exit fullscreen mode

Using Claude Desktop:
Simply say: "Create a new notification API"

Claude automatically uses the scaffold-mcp MCP server and creates your API with proper DI, middleware, and validation.

Before Scaffolding: Adding Features

You: "Add a new repository class for comments"
AI: *creates class without DI decorator*
You: "No, use dependency injection like the other repos"
AI: *adds DI but forgets the @injectable decorator*
You: "Look at TaskRepository as an example"
*More back-and-forth*
Enter fullscreen mode Exit fullscreen mode

After Scaffolding: Adding Features

Using CLI:

# What can I add?
scaffold-mcp scaffold list ./backend/apis/my-api

# Add matching feature
scaffold-mcp scaffold add scaffold-repository \
  --project ./backend/apis/my-api \
  --vars '{"entityName":"Comment","tableName":"comments"}'

# ✓ Perfect pattern match with proper DI
Enter fullscreen mode Exit fullscreen mode

Using Claude Desktop:
"Add a repository for comments to my API"

Claude uses scaffold-mcp to ensure the new repository matches your DI patterns, uses the correct decorators, and follows your coding standards.

Creating Your Own Templates

The real power comes from encoding your team's patterns.

Step 1: Installation

# Install
npm install -g @agiflowai/scaffold-mcp

# Initialize templates
scaffold-mcp init
Enter fullscreen mode Exit fullscreen mode

Step 2: Enable Admin Tools (Claude Desktop)

{
  "mcpServers": {
    "scaffold-mcp": {
      "command": "npx",
      "args": ["-y", "@agiflowai/scaffold-mcp", "mcp-serve", "--admin-enable"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Create Template with AI

Tell Claude:

"Create a boilerplate template called 'injectable-service' in 'backend-templates'
that creates backend services with dependency injection and proper structure"
Enter fullscreen mode Exit fullscreen mode

Claude uses the admin tool to generate:

# templates/backend-templates/scaffold.yaml
boilerplate:
  name: injectable-service
  description: Backend service with DI, config, and proper structure
  targetFolder: backend/apis/my-api/src/services

  variables_schema:
    type: object
    properties:
      serviceName:
        type: string
        pattern: "^[A-Z][a-zA-Z0-9]*$"
        description: Service name in PascalCase
    required:
      - serviceName

  includes:
    - {{ serviceName | camelCase }}Service.ts
    - {{ serviceName | camelCase }}Service.test.ts
Enter fullscreen mode Exit fullscreen mode

Step 4: Create Template Files

Tell Claude:

"Create the service template file with dependency injection, config support, and JSDoc"
Enter fullscreen mode Exit fullscreen mode

Claude generates:

// templates/backend-templates/{{ serviceName | camelCase }}Service.ts.liquid
/**
 * {{ serviceName }} Service
 *
 * DESIGN PRINCIPLES:
 * - Dependency Injection: Use Inversify for loose coupling
 * - Type Safety: Full TypeScript support
 * - Configuration: Use Config service for environment variables
 *
 * PATTERN: Injectable Service
 * - MUST use @injectable() decorator
 * - MUST inject dependencies with @inject(TYPES.*)
 * - MUST document design principles in JSDoc
 */
import { inject, injectable } from 'inversify';
import type { IDatabaseService } from '@agimonai/backend-database';
import type { Config } from '@/services';
import { TYPES } from '@/types';

@injectable()
export class {{ serviceName }}Service {
  constructor(
    @inject(TYPES.Database) private db: IDatabaseService,
    @inject(TYPES.Config) private config: Config,
  ) {
    // Initialization logic
  }

  /**
   * Add your service methods here
   */
  public async execute() {
    // Implementation
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Use Your Template

scaffold-mcp boilerplate create injectable-service \
  --vars '{"serviceName":"Email"}'

# ✓ Created backend/apis/my-api/src/services/emailService.ts
# ✓ Created backend/apis/my-api/src/services/emailService.test.ts
# ✓ All with proper DI, JSDoc, and patterns
Enter fullscreen mode Exit fullscreen mode

Or with Claude Desktop:

"Create a new Email service using our injectable service template"
Enter fullscreen mode Exit fullscreen mode

The Results

After switching to scaffolding:

Before

  • Setup time: Hours of back-and-forth per project
  • Code consistency: Inconsistent across the codebase
  • Review time: Mostly spent on style debates
  • Onboarding: Weeks to learn all the conventions

After

  • Setup time: Minutes per project
  • Code consistency: Enforced by templates
  • Review time: Focused on logic, not style
  • Onboarding: Days instead of weeks

Net result: Dramatically faster initialization, zero convention debates, consistent quality across the entire monorepo.

Best Practices

1. Start Simple, Evolve Gradually

# Week 1: Use community templates
scaffold-mcp add --name nextjs-15 --url https://github.com/AgiFlow/aicode-toolkit

# Weeks 2-4: Observe what you change repeatedly

# Week 5+: Create custom templates for your patterns
Enter fullscreen mode Exit fullscreen mode

2. Use Liquid Filters for Consistency

{% comment %}
✅ Good: Ensure consistent casing with filters
Available filters: pascalCase, camelCase, kebabCase, snakeCase, upperCase
{% endcomment %}
@injectable()
export class {{ serviceName | pascalCase }}Service {
  private readonly logger = createLogger('{{ serviceName | kebabCase }}');
  private readonly TABLE_NAME = '{{ tableName | snakeCase }}';
}

{% comment %}
❌ Bad: Rely on user input casing
{% endcomment %}
export class {{ serviceName }}Service {
  private logger = createLogger('{{ serviceName }}');
  private TABLE = '{{ tableName }}';
}
Enter fullscreen mode Exit fullscreen mode

3. Validate with JSON Schema

# ✅ Good: Enforce format and patterns
properties:
  serviceName:
    type: string
    pattern: "^[A-Z][a-zA-Z0-9]*$"  # Must be PascalCase
    example: "Email"
  port:
    type: number
    minimum: 3000
    maximum: 9999

# ❌ Bad: Accept anything
properties:
  serviceName:
    type: string
  port:
    type: number
Enter fullscreen mode Exit fullscreen mode

4. Document in Templates

instruction: |
  Service created successfully!

  Files created:
  - {{ serviceName | camelCase }}Service.ts: Main service with DI
  - {{ serviceName | camelCase }}Service.test.ts: Test suite

  Next steps:
  1. Register in TYPES: add {{ serviceName }}Service to dependency container
  2. Run `pnpm test` to verify tests pass
  3. Inject: @inject(TYPES.{{ serviceName }}Service)
Enter fullscreen mode Exit fullscreen mode

Getting Started Today

Quick Start (5 minutes)

# 1. Install
npm install -g @agiflowai/scaffold-mcp

# 2. Initialize
scaffold-mcp init

# 3. List templates
scaffold-mcp boilerplate list

# 4. Create project
scaffold-mcp boilerplate create <name> --vars '{"projectName":"my-app"}'
Enter fullscreen mode Exit fullscreen mode

Claude Desktop Setup (2 minutes)

{
  "mcpServers": {
    "scaffold-mcp": {
      "command": "npx",
      "args": ["-y", "@agiflowai/scaffold-mcp", "mcp-serve"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Restart Claude Desktop and say: "What scaffolding templates are available?"

The Path Forward

The future of AI-assisted development isn't about AI writing more code—it's about AI writing the right code, consistently, following your conventions.

Three Levels of Adoption

Level 1: User (Start here)

  • Use existing templates
  • 10x faster setup
  • Guaranteed consistency

Level 2: Customizer (Next step)

  • Adapt templates to your team
  • Encode patterns once, reuse forever
  • Zero convention debates

Level 3: Creator (Advanced)

  • Build custom templates for your stack
  • Advanced generators for complex workflows
  • Share across your organization

The Bottom Line

Stop fighting with AI over conventions. Stop reviewing the same style issues. Stop recreating the same patterns.

Start with templates. Scale with scaffolding.


Resources


"The best code is the code you don't have to write. But when you do write it, scaffolding ensures you write it right the first time—every time."

This is Part 1 of my series on making AI coding assistants work on complex projects. Stay tuned for Part 2!

Questions? I'm happy to discuss architecture patterns, scaffolding strategies, or share more implementation details in the comments.

Top comments (0)