/ 6 min read

Consistent Scaffolding at Scale


This is Part 11 of our series on plan-based development with Claude Code. Today we explore code generators - the automation that keeps a large monorepo consistent.


The Consistency Problem


When you create your fifth package, you’re copying from the fourth. When you create your twentieth, you’re copying from… which one? The one that happened to be open? The newest? The one with the cleanest pattern?


Copy-paste creates drift. Small differences accumulate:

  • Package A has test script, Package B has test:unit
  • Some packages export from ./dist, others from ./src
  • Test setup files are named differently across packages

These inconsistencies compound. Tools that work for one package fail for another. Developers waste time debugging configuration instead of building features.


Generators: Consistency by Construction


Code generators flip the script. Instead of copying and modifying, you describe what you want and let the generator create it correctly:


Terminal window
turbo gen package
# ? Package name: my-feature
# ? Package type: Library
# ? Description: Feature X implementation
# ? Include tests: Yes
# Created packages/my-feature with correct structure

Every package created this way has:

  • Identical script names
  • Consistent export structure
  • Same test configuration
  • Matching TypeScript settings

Turborepo + Plop.js


Turborepo includes built-in generator support powered by Plop.js. Configuration lives in turbo/generators/config.cjs:


module.exports = function generator(plop) {
// Register helpers for case conversion
plop.setHelper("pascalCase", (text) => toPascalCase(text));
plop.setHelper("camelCase", (text) => toCamelCase(text));
// Define generators
plop.setGenerator("package", {
description: "Create a new package",
prompts: [...],
actions: [...],
});
};

Templates live in turbo/generators/templates/ using Handlebars syntax.


Our Five Generator Types


1. Package Generator


Creates new shared packages in packages/:


Terminal window
# Interactive
turbo gen package
# Non-interactive (for automation)
turbo gen package -a "my-pkg" -a "library" -a "My description" -a "true"

Package types:

  • react - UI components with JSX, jsdom test environment
  • library - Pure TypeScript, node test environment
  • schema - Zod schemas with validation patterns
  • config - Shared configuration, no build step

What it creates:

packages/my-pkg/
├── package.json # Correct scripts, dependencies, exports
├── tsconfig.json # Extends base config
├── tsdown.config.ts # Build configuration
├── vitest.config.ts # Test configuration (if enabled)
└── src/
├── index.ts # Main export with example code
└── index.test.ts # Example test (if enabled)

Template example (templates/package/library/package.json.hbs):


{
"name": "@bts/{{name}}",
"version": "0.0.1",
"type": "module",
"description": "{{description}}",
"exports": {
".": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
}
},
"scripts": {
"dev": "tsdown --watch",
"build": "tsdown",
"check-types": "tsc --noEmit"{{#if hasTests}},
"test": "vitest run"{{/if}}
}
}

The {{#if hasTests}} conditional ensures test scripts only appear when tests are enabled.


2. Component Generator


Creates React components in existing UI packages:


Terminal window
turbo gen component -a "core-ui" -a "data-table" -a "true"

Parameters:

  • package - Target UI package (core-ui, form-ui, chart-ui, etc.)
  • name - Component name in kebab-case
  • hasTest - Whether to create a test file

What it creates:

packages/core-ui/src/data-table/
├── data-table.tsx
├── data-table.test.tsx # If hasTest
└── index.ts

And automatically updates packages/core-ui/src/index.ts to export the new component.


3. Router Generator


Creates domain routers for the API layer:


Terminal window
turbo gen router -a "inventory" -a "Inventory management" -a "false" -a "true"

Parameters:

  • domain - Domain name (becomes @bts/inventory-router)
  • description - Router description
  • hasEffect - Include Effect TS integration
  • hasTests - Include test setup

What it creates:

packages/inventory-router/
├── package.json
├── tsconfig.json
├── src/
│ ├── index.ts # Router exports
│ └── inventory.router.ts # Router implementation
└── turbo.json # Task configuration with tags

4. Tool Generator


Creates AI tools for existing AI packages:


Terminal window
turbo gen tool -a "project-ai" -a "archive-project" -a "Archive a project" -a "false" -a "true"

Parameters:

  • domain - AI package (project-ai, results-ai, survey-ai, etc.)
  • name - Tool name in kebab-case
  • description - LLM-visible description
  • hasArtifact - Create streaming artifact for UI
  • isGenerator - Use generator function for streaming progress

What it creates:

packages/project-ai/src/tools/archive-project/
├── archive-project-tool.ts # Tool implementation
├── archive-project-tool.test.ts # Unit tests
└── index.ts # Re-exports

The template includes the full tool pattern:


// Generated tool structure
export const TOOL_NAME = "{{camelCase name}}";
export const {{camelCase name}}Tool = tool({
description: `{{description}}`,
parameters: z.object({
input: z.string().describe("The input to process"),
}),
{{#if isGenerator}}
execute: async function* ({ input }) {
yield { text: "Starting..." };
// Implementation
yield { text: result, forceStop: true };
},
{{else}}
execute: async ({ input }) => {
return { success: true, result };
},
{{/if}}
});

5. AI Package Generator


Creates entirely new AI domain packages:


Terminal window
turbo gen ai-package -a "logistics" -a "Logistics AI tools" -a "true" -a "true" -a "false"

Parameters:

  • domain - Domain name (becomes @bts/logistics-ai)
  • description - Package description
  • hasTools - Include tools directory
  • hasArtifacts - Include artifacts directory
  • hasAgents - Include agents directory

What it creates:

packages/logistics-ai/
├── package.json
├── tsconfig.json
├── vitest.config.ts
└── src/
├── index.ts
├── config.ts
├── types/index.ts
├── tools/ # If hasTools
│ ├── index.ts
│ └── example.ts
├── artifacts/ # If hasArtifacts
│ ├── index.ts
│ └── example.ts
└── agents/ # If hasAgents
├── index.ts
└── example.ts

Interactive vs Non-Interactive Mode


Interactive mode for manual use:

Terminal window
turbo gen package
# Prompts guide you through options

Non-interactive mode for automation/scripts:

Terminal window
turbo gen package -a "name" -a "type" -a "description" -a "hasTests"
# Arguments passed in order, no prompts

This is crucial for:

  • CI pipelines that scaffold before testing
  • Scripts that create multiple packages
  • Claude Code workflows that generate code

Helper Functions for Templates


Handlebars helpers transform names:


plop.setHelper("pascalCase", (text) => toPascalCase(text));
plop.setHelper("camelCase", (text) => toCamelCase(text));
plop.setHelper("constantCase", (text) => toConstantCase(text));

Usage in templates:

// From "my-feature" input:
{{name}} → my-feature
{{pascalCase name}} → MyFeature
{{camelCase name}} → myFeature
{{constantCase name}} → MY_FEATURE

This ensures naming conventions are enforced across generated files.


Keeping Generators in Sync


Generators rot if you don’t maintain them. When patterns evolve:


  1. Update templates first: Change the generator template
  2. Generate a new package: Use the generator to create a test package
  3. Verify it works: Build, test, type-check
  4. Consider migration: Should existing packages be updated?

We update generators when:

  • Adding new dependencies that should be standard
  • Changing build tool configuration
  • Updating test patterns
  • Adding new export patterns

The Workflow with Claude


Claude Code knows about generators from CLAUDE.md:


## Commands
# Generators (always use for new packages)
turbo gen package # New package
turbo gen tool # New AI tool
turbo gen component # New UI component

When you ask Claude to create a new package, it uses the generator:


> Create a new @bts/notifications package for handling push notifications
Claude runs: turbo gen package -a "notifications" -a "library" -a "Push notification handling" -a "true"

This ensures Claude-created packages match hand-created ones exactly.


Benefits Over Manual Creation


Consistency: Every package follows the same structure

Speed: New package in seconds, not minutes of copying/editing

Correctness: No forgotten configurations or typos

Discoverability: turbo gen --help shows available generators

Maintainability: Update one template, affect all future packages


Getting Started with Generators


  1. Create the config file:

    turbo/generators/config.cjs
  2. Add your first generator:

    plop.setGenerator("package", {
    description: "Create a new package",
    prompts: [{ type: "input", name: "name", message: "Name:" }],
    actions: [{ type: "add", path: "packages/{{name}}/package.json", templateFile: "templates/package.json.hbs" }],
    });
  3. Create the template:

    turbo/generators/templates/package.json.hbs
  4. Test it:

    Terminal window
    turbo gen package

Start simple - one generator, one template. Add complexity as patterns emerge.




Code generators transform package creation from error-prone copying to reliable automation. In a monorepo with 60+ packages, this consistency isn’t optional - it’s essential.


Next up in Part 12: Effect TS patterns - structured errors and dependency injection for maintainable services.