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
testscript, Package B hastest: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:
turbo gen package# ? Package name: my-feature# ? Package type: Library# ? Description: Feature X implementation# ? Include tests: Yes# Created packages/my-feature with correct structureEvery 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/:
# Interactiveturbo 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 environmentlibrary- Pure TypeScript, node test environmentschema- Zod schemas with validation patternsconfig- 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:
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-casehasTest- 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.tsAnd automatically updates packages/core-ui/src/index.ts to export the new component.
3. Router Generator
Creates domain routers for the API layer:
turbo gen router -a "inventory" -a "Inventory management" -a "false" -a "true"Parameters:
domain- Domain name (becomes @bts/inventory-router)description- Router descriptionhasEffect- Include Effect TS integrationhasTests- 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 tags4. Tool Generator
Creates AI tools for existing AI packages:
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-casedescription- LLM-visible descriptionhasArtifact- Create streaming artifact for UIisGenerator- 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-exportsThe template includes the full tool pattern:
// Generated tool structureexport 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:
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 descriptionhasTools- Include tools directoryhasArtifacts- Include artifacts directoryhasAgents- 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.tsInteractive vs Non-Interactive Mode
Interactive mode for manual use:
turbo gen package# Prompts guide you through optionsNon-interactive mode for automation/scripts:
turbo gen package -a "name" -a "type" -a "description" -a "hasTests"# Arguments passed in order, no promptsThis 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_FEATUREThis ensures naming conventions are enforced across generated files.
Keeping Generators in Sync
Generators rot if you don’t maintain them. When patterns evolve:
- Update templates first: Change the generator template
- Generate a new package: Use the generator to create a test package
- Verify it works: Build, test, type-check
- 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 packageturbo gen tool # New AI toolturbo gen component # New UI componentWhen 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
-
Create the config file:
turbo/generators/config.cjs -
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" }],}); -
Create the template:
turbo/generators/templates/package.json.hbs -
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.