Code Generation & Templates Overview
Comprehensive guide to code generation techniques including template engines, AST manipulation, code scaffolding, and automated boilerplate generation for increased productivity and consistency.
When to Use Scaffolding new projects or components Generating repetitive boilerplate code Creating CRUD operations automatically Generating API clients from OpenAPI specs Building code from templates Creating database models from schemas Generating TypeScript types from JSON Schema Building custom CLI generators Instructions 1. Template Engines Handlebars Templates // templates/component.hbs import React from 'react';
export interface {{pascalCase name}}Props { {{#each props}} {{this.name}}{{#if this.optional}}?{{/if}}: {{this.type}}; {{/each}} }
export const {{pascalCase name}}: React.FC<{{pascalCase name}}Props> = ({ {{#each props}}{{this.name}},{{/each}} }) => { return (
// generator.ts import Handlebars from 'handlebars'; import fs from 'fs';
// Register helpers Handlebars.registerHelper('pascalCase', (str: string) => str.replace(/(\w)(\w*)/g, (_, first, rest) => first.toUpperCase() + rest.toLowerCase() ) );
Handlebars.registerHelper('kebabCase', (str: string) => str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase() );
// Load template const templateSource = fs.readFileSync('templates/component.hbs', 'utf8'); const template = Handlebars.compile(templateSource);
// Generate code const code = template({ name: 'userProfile', props: [ { name: 'userId', type: 'string', optional: false }, { name: 'onUpdate', type: '() => void', optional: true } ] });
fs.writeFileSync('src/components/UserProfile.tsx', code);
EJS Templates // templates/api-endpoint.ejs import { Router } from 'express'; import { <%= modelName %>Service } from '../services/<%= kebabCase(modelName) %>.service';
const router = Router(); const service = new <%= modelName %>Service();
// GET /<%= pluralize(kebabCase(modelName)) %> router.get('/', async (req, res) => { try { const items = await service.findAll(); res.json(items); } catch (error) { res.status(500).json({ error: error.message }); } });
// GET /<%= pluralize(kebabCase(modelName)) %>/:id router.get('/:id', async (req, res) => { try { const item = await service.findById(req.params.id); if (!item) { return res.status(404).json({ error: 'Not found' }); } res.json(item); } catch (error) { res.status(500).json({ error: error.message }); } });
// POST /<%= pluralize(kebabCase(modelName)) %> router.post('/', async (req, res) => { try { const item = await service.create(req.body); res.status(201).json(item); } catch (error) { res.status(400).json({ error: error.message }); } });
export default router;
// Using EJS import ejs from 'ejs';
const code = await ejs.renderFile('templates/api-endpoint.ejs', { modelName: 'User', kebabCase: (str: string) => str.replace(/([A-Z])/g, '-$1').toLowerCase().slice(1), pluralize: (str: string) => str + 's' });
- AST-Based Code Generation Using Babel/TypeScript AST // ast-generator.ts import * as ts from 'typescript';
export class TypeScriptGenerator { // Generate interface generateInterface(name: string, properties: Array<{ name: string; type: string; optional?: boolean }>) { const members = properties.map(prop => ts.factory.createPropertySignature( undefined, ts.factory.createIdentifier(prop.name), prop.optional ? ts.factory.createToken(ts.SyntaxKind.QuestionToken) : undefined, ts.factory.createTypeReferenceNode(prop.type) ) );
const interfaceDecl = ts.factory.createInterfaceDeclaration(
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier(name),
undefined,
undefined,
members
);
return this.printNode(interfaceDecl);
}
// Generate class generateClass(name: string, properties: Array<{ name: string; type: string }>) { const propertyDecls = properties.map(prop => ts.factory.createPropertyDeclaration( [ts.factory.createToken(ts.SyntaxKind.PrivateKeyword)], ts.factory.createIdentifier(prop.name), undefined, ts.factory.createTypeReferenceNode(prop.type), undefined ) );
const constructor = ts.factory.createConstructorDeclaration(
undefined,
properties.map(prop =>
ts.factory.createParameterDeclaration(
undefined,
undefined,
ts.factory.createIdentifier(prop.name),
undefined,
ts.factory.createTypeReferenceNode(prop.type)
)
),
ts.factory.createBlock(
properties.map(prop =>
ts.factory.createExpressionStatement(
ts.factory.createBinaryExpression(
ts.factory.createPropertyAccessExpression(
ts.factory.createThis(),
prop.name
),
ts.SyntaxKind.EqualsToken,
ts.factory.createIdentifier(prop.name)
)
)
),
true
)
);
const classDecl = ts.factory.createClassDeclaration(
[ts.factory.createToken(ts.SyntaxKind.ExportKeyword)],
ts.factory.createIdentifier(name),
undefined,
undefined,
[...propertyDecls, constructor]
);
return this.printNode(classDecl);
}
private printNode(node: ts.Node): string { const sourceFile = ts.createSourceFile( 'temp.ts', '', ts.ScriptTarget.Latest, false, ts.ScriptKind.TS );
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
return printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
} }
// Usage const generator = new TypeScriptGenerator();
const interfaceCode = generator.generateInterface('User', [ { name: 'id', type: 'string' }, { name: 'email', type: 'string' }, { name: 'name', type: 'string', optional: true } ]);
const classCode = generator.generateClass('UserService', [ { name: 'repository', type: 'UserRepository' }, { name: 'logger', type: 'Logger' } ]);
- Project Scaffolding Simple CLI Generator // cli/generate.ts
!/usr/bin/env node
import { Command } from 'commander'; import inquirer from 'inquirer'; import fs from 'fs-extra'; import path from 'path';
const program = new Command();
program .name('generate') .description('Code generator CLI') .version('1.0.0');
program
.command('component
await generateComponent(name, options.dir, answers);
});
program
.command('api
program.parse();
async function generateComponent(name: string, dir: string, options: any) { const componentName = pascalCase(name); const ext = options.typescript ? 'tsx' : 'jsx';
const template = options.type === 'functional' ? getFunctionalComponentTemplate(componentName, options.typescript) : getClassComponentTemplate(componentName, options.typescript);
const componentPath = path.join(dir, ${componentName}.${ext});
await fs.ensureDir(dir); await fs.writeFile(componentPath, template);
console.log(✓ Created ${componentPath});
if (options.test) {
const testTemplate = getTestTemplate(componentName, options.typescript);
const testPath = path.join(dir, ${componentName}.test.${ext});
await fs.writeFile(testPath, testTemplate);
console.log(✓ Created ${testPath});
}
}
function getFunctionalComponentTemplate(name: string, ts: boolean): string { if (ts) { return `import React from 'react';
export interface ${name}Props { // Add props here }
export const ${name}: React.FC<${name}Props> = (props) => { return (
${name}
return `import React from 'react';
export const ${name} = (props) => { return (
${name}
async function generateApiResource(resource: string) { const name = pascalCase(resource);
// Generate model
const modelCode = export interface ${name} {
id: string;
createdAt: Date;
updatedAt: Date;
// Add fields here
};
await fs.writeFile(src/models/${kebabCase(resource)}.model.ts, modelCode);
// Generate service const serviceCode = `import { ${name} } from '../models/${kebabCase(resource)}.model';
export class ${name}Service { async findAll(): Promise<${name}[]> { // Implement return []; }
async findById(id: string): Promise<${name} | null> { // Implement return null; }
async create(data: Partial<${name}>): Promise<${name}> { // Implement throw new Error('Not implemented'); }
async update(id: string, data: Partial<${name}>): Promise<${name}> { // Implement throw new Error('Not implemented'); }
async delete(id: string): Promise;
await fs.writeFile(src/services/${kebabCase(resource)}.service.ts`, serviceCode);
// Generate controller const controllerCode = `import { Router } from 'express'; import { ${name}Service } from '../services/${kebabCase(resource)}.service';
const router = Router(); const service = new ${name}Service();
router.get('/', async (req, res) => { const items = await service.findAll(); res.json(items); });
router.get('/:id', async (req, res) => { const item = await service.findById(req.params.id); if (!item) return res.status(404).json({ error: 'Not found' }); res.json(item); });
router.post('/', async (req, res) => { const item = await service.create(req.body); res.status(201).json(item); });
router.put('/:id', async (req, res) => { const item = await service.update(req.params.id, req.body); res.json(item); });
router.delete('/:id', async (req, res) => { await service.delete(req.params.id); res.status(204).send(); });
export default router;
;
await fs.writeFile(src/controllers/${kebabCase(resource)}.controller.ts`, controllerCode);
console.log(✓ Generated API resource: ${name});
}
- OpenAPI Client Generation // openapi-client-generator.ts import SwaggerParser from '@apidevtools/swagger-parser'; import { compile } from 'json-schema-to-typescript';
export class OpenAPIClientGenerator { async generate(specPath: string, outputDir: string) { const api = await SwaggerParser.parse(specPath);
// Generate TypeScript types from schemas
if (api.components?.schemas) {
for (const [name, schema] of Object.entries(api.components.schemas)) {
const ts = await compile(schema as any, name, {
bannerComment: ''
});
await fs.writeFile(
path.join(outputDir, 'types', `${name}.ts`),
ts
);
}
}
// Generate API client methods
for (const [path, pathItem] of Object.entries(api.paths)) {
for (const [method, operation] of Object.entries(pathItem)) {
if (['get', 'post', 'put', 'delete', 'patch'].includes(method)) {
const clientMethod = this.generateClientMethod(
method,
path,
operation as any
);
// Write to file...
}
}
}
}
private generateClientMethod( method: string, path: string, operation: any ): string { const functionName = operation.operationId || this.pathToFunctionName(method, path); const parameters = operation.parameters || [];
return `
async ${functionName}(${this.generateParameters(parameters)}): Promise<${this.getResponseType(operation)}> { const response = await this.request('${method.toUpperCase()}', '${path}', { ${this.generateRequestOptions(parameters)} }); return response.json(); } `; }
private generateParameters(parameters: any[]): string {
return parameters
.map(p => ${p.name}${p.required ? '' : '?'}: ${this.schemaToType(p.schema)})
.join(', ');
}
private getResponseType(operation: any): string { const successResponse = operation.responses['200'] || operation.responses['201']; if (!successResponse) return 'any';
const schema = successResponse.content?.['application/json']?.schema;
return schema ? this.schemaToType(schema) : 'any';
}
private schemaToType(schema: any): string {
if (schema.$ref) {
return schema.$ref.split('/').pop();
}
if (schema.type === 'string') return 'string';
if (schema.type === 'number' || schema.type === 'integer') return 'number';
if (schema.type === 'boolean') return 'boolean';
if (schema.type === 'array') return ${this.schemaToType(schema.items)}[];
return 'any';
}
private pathToFunctionName(method: string, path: string): string {
const cleanPath = path.replace(/{.*?}/g, 'By').replace(/[^a-zA-Z0-9]/g, '');
return ${method}${cleanPath};
}
}
- Database Model Generation // prisma-schema-generator.ts export class PrismaSchemaGenerator { generateModel(table: DatabaseTable): string { return `model ${pascalCase(table.name)} { ${table.columns.map(col => this.generateField(col)).join('\n')}
${this.generateRelations(table.relations)} ${this.generateIndexes(table.indexes)} } `; }
private generateField(column: Column): string {
const optional = !column.required ? '?' : '';
const unique = column.unique ? ' @unique' : '';
const defaultValue = column.default ? @default(${column.default}) : '';
return ` ${column.name} ${this.mapType(column.type)}${optional}${unique}${defaultValue}`;
}
private mapType(sqlType: string): string {
const typeMap: Record
private generateRelations(relations: Relation[]): string {
return relations.map(rel => {
if (rel.type === 'hasMany') {
return ${rel.name} ${rel.model}[];
} else if (rel.type === 'belongsTo') {
return ${rel.name} ${rel.model} @relation(fields: [${rel.foreignKey}], references: [id]);
}
return '';
}).join('\n');
}
}
- GraphQL Code Generation // graphql-codegen.config.ts import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = { schema: 'http://localhost:4000/graphql', documents: ['src//*.tsx', 'src//*.ts'], generates: { './src/generated/graphql.ts': { plugins: [ 'typescript', 'typescript-operations', 'typescript-react-apollo' ], config: { withHooks: true, withComponent: false, withHOC: false } }, './src/generated/introspection.json': { plugins: ['introspection'] } } };
export default config;
- Plop.js Generator // plopfile.ts import { NodePlopAPI } from 'plop';
export default function (plop: NodePlopAPI) { // Component generator plop.setGenerator('component', { description: 'React component', prompts: [ { type: 'input', name: 'name', message: 'Component name:' }, { type: 'list', name: 'type', message: 'Component type:', choices: ['functional', 'class'] } ], actions: [ { type: 'add', path: 'src/components/{{pascalCase name}}/{{pascalCase name}}.tsx', templateFile: 'templates/component.hbs' }, { type: 'add', path: 'src/components/{{pascalCase name}}/{{pascalCase name}}.test.tsx', templateFile: 'templates/component.test.hbs' }, { type: 'add', path: 'src/components/{{pascalCase name}}/index.ts', template: "export { {{pascalCase name}} } from './{{pascalCase name}}';\n" } ] });
// API generator plop.setGenerator('api', { description: 'API endpoint with full stack', prompts: [ { type: 'input', name: 'name', message: 'Resource name (e.g., user, post):' } ], actions: [ { type: 'add', path: 'src/models/{{kebabCase name}}.model.ts', templateFile: 'templates/model.hbs' }, { type: 'add', path: 'src/services/{{kebabCase name}}.service.ts', templateFile: 'templates/service.hbs' }, { type: 'add', path: 'src/controllers/{{kebabCase name}}.controller.ts', templateFile: 'templates/controller.hbs' }, { type: 'add', path: 'src/routes/{{kebabCase name}}.routes.ts', templateFile: 'templates/routes.hbs' } ] }); }
Best Practices ✅ DO Use templates for repetitive code patterns Generate TypeScript types from schemas Include tests in generated code Follow project conventions in templates Add comments to explain generated code Version control your templates Make templates configurable Generate documentation alongside code Validate inputs before generating Use consistent naming conventions Keep templates simple and maintainable Provide CLI for easy generation ❌ DON'T Over-generate (avoid unnecessary complexity) Generate code that's hard to maintain Forget to validate generated code Hardcode values in templates Generate code without documentation Create generators for one-off use cases Mix business logic in templates Generate code without formatting Skip error handling in generators Create overly complex templates Common Patterns Pattern 1: CRUD Generator export function generateCRUD(entityName: string) { return { model: generateModel(entityName), service: generateService(entityName), controller: generateController(entityName), routes: generateRoutes(entityName), tests: generateTests(entityName) }; }
Pattern 2: Migration Generator export function generateMigration(name: string, changes: SchemaChange[]) { return { up: generateUpMigration(changes), down: generateDownMigration(changes) }; }
Pattern 3: Factory Generator
export function generateFactory(model: Model) {
return export const create${model.name} = (overrides?: Partial<${model.name}>): ${model.name} => ({
${model.fields.map(f =>${f.name}: ${getDefaultValue(f)}).join(',\n ')},
...overrides
});;
}
Tools & Resources Plop: Micro-generator framework Yeoman: Scaffolding tool Hygen: Code generator with templates GraphQL Code Generator: Generate code from GraphQL Prisma: Database ORM with code generation OpenAPI Generator: Generate clients from OpenAPI json-schema-to-typescript: Generate TS types TypeScript Compiler API: AST manipulation