Welcome to Part 3 of our CLI generator series! Now we’ll dive into the core template generation system using Mustache.js to create dynamic, production-ready code.
Template Generation Architecture
Our template system needs to be flexible enough to generate different combinations of services while maintaining clean, readable code. We’ll use Mustache.js for its simplicity and power.
flowchart TD A[CLI Input] --> B[Template Generator] B --> C{Services Selected?} C -->|HTTP| D[HTTP Service Template] C -->|Kafka| E[Kafka Service Template] C -->|Redis| F[Redis Service Template] D --> G[Mustache Renderer] E --> G F --> G G --> H[Generated Files] H --> I[Package.json] H --> J[Source Files] H --> K[Config Files] H --> L[Docker Files] style A fill:#e1f5fe style B fill:#f3e5f5 style G fill:#e8f5e8 style H fill:#fff3e0
Template Generator Implementation
Let’s create the core template generation logic:
import fs from 'fs-extra';
import path from 'path';
import Mustache from 'mustache';
export class TemplateGenerator {
private templatesDir: string;
constructor(templatesDir: string) {
this.templatesDir = templatesDir;
}
async generateProject(config: ProjectConfig, outputDir: string) {
await fs.ensureDir(outputDir);
// Generate package.json
await this.generatePackageJson(config, outputDir);
// Generate source files
await this.generateSourceFiles(config, outputDir);
// Generate configuration files
await this.generateConfigFiles(config, outputDir);
// Generate Docker files
await this.generateDockerFiles(config, outputDir);
}
private async generatePackageJson(config: ProjectConfig, outputDir: string) {
const template = await fs.readFile(
path.join(this.templatesDir, 'package.json.mustache'),
'utf8'
);
const packageData = {
name: config.name,
description: config.description,
useTypeScript: config.useTypeScript,
enableHttp: config.enableHttp,
enableKafka: config.enableKafka,
enableRedis: config.enableRedis
};
const output = Mustache.render(template, packageData);
await fs.writeFile(path.join(outputDir, 'package.json'), output);
}
}
Dynamic Package.json Template
Here’s our smart package.json template that includes dependencies based on selected services:
{
"name": "{{name}}",
"version": "1.0.0",
"description": "{{description}}",
"main": "{{#useTypeScript}}dist/index.js{{/useTypeScript}}{{^useTypeScript}}src/index.js{{/useTypeScript}}",
{{#useTypeScript}}"types": "dist/index.d.ts",{{/useTypeScript}}
"scripts": {
{{#useTypeScript}}"build": "tsc",{{/useTypeScript}}
{{#useTypeScript}}"dev": "tsx watch src/index.ts",{{/useTypeScript}}
{{^useTypeScript}}"dev": "nodemon src/index.js",{{/useTypeScript}}
"start": "node {{#useTypeScript}}dist/index.js{{/useTypeScript}}{{^useTypeScript}}src/index.js{{/useTypeScript}}",
"test": "jest",
"lint": "eslint src/**/*.{{#useTypeScript}}ts{{/useTypeScript}}{{^useTypeScript}}js{{/useTypeScript}}"
},
"dependencies": {
"dotenv": "^16.3.1"{{#enableHttp}},
"express": "^4.18.2",
"cors": "^2.8.5",
"helmet": "^7.0.0"{{/enableHttp}}{{#enableKafka}},
"kafkajs": "^2.2.4"{{/enableKafka}}{{#enableRedis}},
"redis": "^4.6.7"{{/enableRedis}}
},
"devDependencies": {
{{#useTypeScript}}"typescript": "^5.2.0",
"@types/node": "^20.8.0",
"tsx": "^3.14.0"{{#enableHttp}},
"@types/express": "^4.17.17",
"@types/cors": "^2.8.13"{{/enableHttp}}{{/useTypeScript}}
{{^useTypeScript}}"nodemon": "^3.0.1"{{/useTypeScript}},
"eslint": "^8.50.0",
"jest": "^29.7.0"
}
}
HTTP Service Template
Let’s create a production-ready HTTP service template with proper middleware and error handling:
{{#useTypeScript}}import express, { Request, Response, Application } from 'express';
import cors from 'cors';
import helmet from 'helmet';
export class HttpService {
private app: Application;
private port: number;
constructor(port: number = {{httpPort}}) {
this.app = express();
this.port = port;
this.setupMiddleware();
this.setupRoutes();
}
private setupMiddleware(): void {
this.app.use(helmet());
this.app.use(cors());
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
}
private setupRoutes(): void {
this.app.get('/health', (req: Request, res: Response) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
service: '{{name}}'
});
});
this.app.get('/', (req: Request, res: Response) => {
res.json({
message: 'Welcome to {{name}}',
version: '1.0.0'
});
});
}
async start(): Promise<void> {
return new Promise((resolve) => {
this.app.listen(this.port, () => {
console.log(`HTTP server running on port ${this.port}`);
resolve();
});
});
}
getApp(): Application {
return this.app;
}
}{{/useTypeScript}}
Kafka Service Template
Here’s a robust Kafka service template with proper error handling and reconnection logic:
{{#enableKafka}}{{#useTypeScript}}import { Kafka, Producer, Consumer, KafkaMessage } from 'kafkajs';
export class KafkaService {
private kafka: Kafka;
private producer: Producer;
private consumer: Consumer;
private isConnected: boolean = false;
constructor(brokers: string[] = ['{{kafkaBroker}}']) {
this.kafka = new Kafka({
clientId: '{{name}}',
brokers,
retry: {
initialRetryTime: 100,
retries: 8
}
});
this.producer = this.kafka.producer();
this.consumer = this.kafka.consumer({
groupId: '{{name}}-group'
});
}
async connect(): Promise<void> {
try {
await this.producer.connect();
await this.consumer.connect();
this.isConnected = true;
console.log('Kafka connected successfully');
} catch (error) {
console.error('Failed to connect to Kafka:', error);
throw error;
}
}
async publishMessage(topic: string, message: any): Promise<void> {
if (!this.isConnected) {
throw new Error('Kafka not connected');
}
await this.producer.send({
topic,
messages: [{
key: String(Date.now()),
value: JSON.stringify(message),
timestamp: String(Date.now())
}]
});
}
async subscribe(topic: string, handler: (message: KafkaMessage) => Promise<void>): Promise<void> {
if (!this.isConnected) {
throw new Error('Kafka not connected');
}
await this.consumer.subscribe({ topic });
await this.consumer.run({
eachMessage: async ({ message }) => {
try {
await handler(message);
} catch (error) {
console.error('Error processing message:', error);
}
}
});
}
async disconnect(): Promise<void> {
await this.producer.disconnect();
await this.consumer.disconnect();
this.isConnected = false;
console.log('Kafka disconnected');
}
}{{/useTypeScript}}{{/enableKafka}}
Redis Service Template
A Redis service template with connection pooling and error handling:
{{#enableRedis}}{{#useTypeScript}}import { createClient, RedisClientType } from 'redis';
export class RedisService {
private client: RedisClientType;
private isConnected: boolean = false;
constructor(url: string = '{{redisUrl}}') {
this.client = createClient({
url,
retry_delay_on_failure: 100,
max_attempts: 3
});
this.client.on('error', (err) => {
console.error('Redis Client Error:', err);
});
this.client.on('connect', () => {
console.log('Redis client connected');
});
this.client.on('ready', () => {
this.isConnected = true;
console.log('Redis client ready');
});
}
async connect(): Promise<void> {
try {
await this.client.connect();
} catch (error) {
console.error('Failed to connect to Redis:', error);
throw error;
}
}
async set(key: string, value: any, ttl?: number): Promise<void> {
if (!this.isConnected) {
throw new Error('Redis not connected');
}
const serializedValue = JSON.stringify(value);
if (ttl) {
await this.client.setEx(key, ttl, serializedValue);
} else {
await this.client.set(key, serializedValue);
}
}
async get<T = any>(key: string): Promise<T | null> {
if (!this.isConnected) {
throw new Error('Redis not connected');
}
const value = await this.client.get(key);
return value ? JSON.parse(value) : null;
}
async delete(key: string): Promise<void> {
if (!this.isConnected) {
throw new Error('Redis not connected');
}
await this.client.del(key);
}
async disconnect(): Promise<void> {
await this.client.disconnect();
this.isConnected = false;
console.log('Redis disconnected');
}
}{{/useTypeScript}}{{/enableRedis}}
Main Application Template
The main index file that orchestrates all services with graceful shutdown:
{{#useTypeScript}}import 'dotenv/config';
{{#enableHttp}}import { HttpService } from './services/http';{{/enableHttp}}
{{#enableKafka}}import { KafkaService } from './services/kafka';{{/enableKafka}}
{{#enableRedis}}import { RedisService } from './services/redis';{{/enableRedis}}
class Application {
{{#enableHttp}}private httpService: HttpService;{{/enableHttp}}
{{#enableKafka}}private kafkaService: KafkaService;{{/enableKafka}}
{{#enableRedis}}private redisService: RedisService;{{/enableRedis}}
constructor() {
{{#enableHttp}}this.httpService = new HttpService(Number(process.env.PORT) || {{httpPort}});{{/enableHttp}}
{{#enableKafka}}this.kafkaService = new KafkaService();{{/enableKafka}}
{{#enableRedis}}this.redisService = new RedisService();{{/enableRedis}}
}
async start(): Promise<void> {
try {
console.log('Starting {{name}}...');
{{#enableRedis}}// Connect to Redis
await this.redisService.connect();{{/enableRedis}}
{{#enableKafka}}// Connect to Kafka
await this.kafkaService.connect();
// Setup Kafka message handling
await this.kafkaService.subscribe('{{name}}-events', async (message) => {
console.log('Received message:', message.value?.toString());
});{{/enableKafka}}
{{#enableHttp}}// Start HTTP server
await this.httpService.start();{{/enableHttp}}
console.log('{{name}} started successfully');
} catch (error) {
console.error('Failed to start application:', error);
process.exit(1);
}
}
async shutdown(): Promise<void> {
console.log('Shutting down gracefully...');
{{#enableKafka}}await this.kafkaService.disconnect();{{/enableKafka}}
{{#enableRedis}}await this.redisService.disconnect();{{/enableRedis}}
console.log('Shutdown complete');
process.exit(0);
}
}
const app = new Application();
// Graceful shutdown handling
process.on('SIGTERM', () => app.shutdown());
process.on('SIGINT', () => app.shutdown());
// Start the application
app.start().catch((error) => {
console.error('Application failed to start:', error);
process.exit(1);
});{{/useTypeScript}}
Environment Configuration Template
Generate a comprehensive .env.example file:
# {{name}} Configuration
# Application
NODE_ENV=development
LOG_LEVEL=info
{{#enableHttp}}# HTTP Server
PORT={{httpPort}}
{{/enableHttp}}
{{#enableKafka}}# Kafka Configuration
KAFKA_BROKERS={{kafkaBroker}}
KAFKA_CLIENT_ID={{name}}
KAFKA_GROUP_ID={{name}}-group
{{/enableKafka}}
{{#enableRedis}}# Redis Configuration
REDIS_URL={{redisUrl}}
REDIS_PASSWORD=
REDIS_DB=0
{{/enableRedis}}
# Health Check
HEALTH_CHECK_INTERVAL=30000
Docker Configuration
Generate Docker and docker-compose files for easy deployment:
version: '3.8'
services:
{{name}}:
build: .
ports:
{{#enableHttp}}- "{{httpPort}}:{{httpPort}}"{{/enableHttp}}
environment:
- NODE_ENV=production
{{#enableHttp}}- PORT={{httpPort}}{{/enableHttp}}
{{#enableKafka}}- KAFKA_BROKERS=kafka:9092{{/enableKafka}}
{{#enableRedis}}- REDIS_URL=redis://redis:6379{{/enableRedis}}
depends_on:
{{#enableKafka}}- kafka{{/enableKafka}}
{{#enableRedis}}- redis{{/enableRedis}}
{{#enableKafka}}kafka:
image: confluentinc/cp-kafka:latest
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
depends_on:
- zookeeper
zookeeper:
image: confluentinc/cp-zookeeper:latest
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000{{/enableKafka}}
{{#enableRedis}}redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
volumes:
redis_data:{{/enableRedis}}
What’s Coming in Part 4?
In our final part, we’ll cover advanced features and publishing the CLI tool:
- Testing the generated templates
- Adding custom middleware templates
- Publishing to npm registry
- Versioning and updates
- Advanced configuration options
Our template system is now capable of generating production-ready microservices with proper architecture and best practices!