@re-shell/cli
Version:
Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja
1,905 lines (1,641 loc) • 50.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.moleculerTemplate = void 0;
exports.moleculerTemplate = {
id: 'moleculer',
name: 'Moleculer',
displayName: 'Moleculer',
description: 'Fast & powerful microservices framework with built-in service discovery, load balancing, and fault tolerance',
language: 'typescript',
framework: 'moleculer',
version: '0.14.0',
tags: ['microservices', 'distributed', 'fault-tolerant'],
port: 3000,
dependencies: {},
features: ['microservices', 'service-discovery', 'load-balancing', 'fault-tolerance'],
files: {
'package.json': `{
"name": "moleculer-microservices",
"version": "1.0.0",
"description": "Moleculer-based microservices with TypeScript",
"main": "dist/index.js",
"scripts": {
"build": "tsc",
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"start": "node dist/index.js",
"start:services": "concurrently \\"npm run service:api\\" \\"npm run service:greeter\\" \\"npm run service:db\\"",
"service:api": "nodemon --exec ts-node src/services/api.service.ts",
"service:greeter": "nodemon --exec ts-node src/services/greeter.service.ts",
"service:db": "nodemon --exec ts-node src/services/db.service.ts",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src/**/*.ts",
"docker:build": "docker-compose build",
"docker:up": "docker-compose up -d",
"docker:down": "docker-compose down"
},
"dependencies": {
"moleculer": "^0.14.32",
"moleculer-web": "^0.10.6",
"moleculer-db": "^0.8.23",
"moleculer-db-adapter-mongo": "^0.4.17",
"moleculer-db-adapter-sequelize": "^0.2.17",
"moleculer-repl": "^0.7.4",
"nats": "^2.18.0",
"ioredis": "^5.3.2",
"jaeger-client": "^3.19.0",
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.2",
"dotenv": "^16.3.1",
"sequelize": "^6.35.2",
"pg": "^8.11.3",
"mongodb": "^6.3.0"
},
"devDependencies": {
"@types/node": "^20.10.5",
"@types/jest": "^29.5.11",
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.5",
"typescript": "^5.3.3",
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0",
"nodemon": "^3.0.2",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@typescript-eslint/eslint-plugin": "^6.15.0",
"@typescript-eslint/parser": "^6.15.0",
"eslint": "^8.56.0",
"concurrently": "^8.2.2"
}
}`,
'tsconfig.json': `{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}`,
'.env.example': `# Node environment
NODE_ENV=development
# Service configuration
SERVICEDIR=dist/services
TRANSPORTER=nats://localhost:4222
CACHER=redis://localhost:6379
# API Gateway
API_PORT=3000
API_HOST=0.0.0.0
# Database
MONGO_URI=mongodb://localhost:27017/moleculer-db
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DB=moleculer
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
# NATS
NATS_URL=nats://localhost:4222
# Authentication
JWT_SECRET=your-secret-key-here
JWT_EXPIRY=7d
# Tracing
JAEGER_AGENT_HOST=localhost
JAEGER_AGENT_PORT=6832
# Metrics
METRICS_ENABLED=true
METRICS_PORT=3030`,
'moleculer.config.ts': `import { BrokerOptions, Errors } from 'moleculer';
import { config } from 'dotenv';
config();
const brokerConfig: BrokerOptions = {
namespace: 'microservices',
nodeID: \`node-\${process.pid}\`,
// Logger configuration
logger: {
type: 'Console',
options: {
colors: true,
moduleColors: true,
formatter: 'full',
objectPrinter: null,
autoPadding: false
}
},
logLevel: 'info',
// Transporter configuration
transporter: process.env.TRANSPORTER || 'TCP',
// Cacher configuration
cacher: {
type: 'Redis',
options: {
redis: process.env.REDIS_HOST ? {
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT || '6379')
} : 'redis://localhost:6379'
}
},
// Serializer configuration
serializer: 'JSON',
// Request timeout
requestTimeout: 10 * 1000,
// Retry policy
retryPolicy: {
enabled: true,
retries: 5,
delay: 100,
maxDelay: 1000,
factor: 2,
check: (err: Errors.MoleculerError) => err && !!err.retryable
},
// Circuit breaker
circuitBreaker: {
enabled: true,
threshold: 0.5,
minRequestCount: 20,
windowTime: 60,
halfOpenTime: 10 * 1000,
check: (err: Errors.MoleculerError) => err && err.code >= 500
},
// Bulkhead
bulkhead: {
enabled: true,
concurrency: 10,
maxQueueSize: 100
},
// Service registry
registry: {
strategy: 'RoundRobin',
preferLocal: true
},
// Metrics
metrics: {
enabled: process.env.METRICS_ENABLED === 'true',
reporter: {
type: 'Prometheus',
options: {
port: parseInt(process.env.METRICS_PORT || '3030'),
path: '/metrics',
defaultLabels: registry => ({
namespace: registry.broker.namespace,
nodeID: registry.broker.nodeID
})
}
}
},
// Tracing
tracing: {
enabled: true,
exporter: {
type: 'Jaeger',
options: {
endpoint: null,
host: process.env.JAEGER_AGENT_HOST || 'localhost',
port: parseInt(process.env.JAEGER_AGENT_PORT || '6832'),
sampler: {
type: 'Const',
options: {}
},
tracerOptions: {},
defaultTags: null
}
}
},
// Middleware
middlewares: [],
// Service created lifecycle event handler
created(broker) {
broker.logger.info('Broker created!');
},
// Service started lifecycle event handler
started(broker) {
broker.logger.info('Broker started!');
},
// Service stopped lifecycle event handler
stopped(broker) {
broker.logger.info('Broker stopped!');
}
};
export default brokerConfig;`,
'src/index.ts': `import { ServiceBroker } from 'moleculer';
import brokerConfig from '../moleculer.config';
import { resolve } from 'path';
async function startBroker() {
const broker = new ServiceBroker(brokerConfig);
// Load services
broker.loadServices(resolve(__dirname, 'services'), '**/*.service.ts');
// Start broker
await broker.start();
// Start REPL for development
if (process.env.NODE_ENV === 'development') {
broker.repl();
}
}
startBroker().catch(err => {
console.error('Failed to start broker:', err);
process.exit(1);
});`,
'src/services/api.service.ts': `import { Service, ServiceBroker, Context } from 'moleculer';
import ApiGateway from 'moleculer-web';
import { IncomingMessage, ServerResponse } from 'http';
import jwt from 'jsonwebtoken';
interface Meta {
user?: {
id: string;
email: string;
role: string;
};
}
export default class ApiService extends Service {
public constructor(broker: ServiceBroker) {
super(broker);
this.parseServiceSchema({
name: 'api',
mixins: [ApiGateway],
settings: {
port: process.env.API_PORT || 3000,
host: process.env.API_HOST || '0.0.0.0',
// Global CORS settings
cors: {
origin: '*',
methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
exposedHeaders: ['X-Total-Count'],
credentials: true,
maxAge: 3600
},
// Rate limiter
rateLimit: {
window: 10 * 1000,
limit: 10,
headers: true,
key: (req: IncomingMessage) => {
return req.headers['x-forwarded-for'] ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
'unknown';
}
},
routes: [
{
path: '/api',
authorization: true,
aliases: {
// Authentication
'POST /auth/login': 'auth.login',
'POST /auth/register': 'auth.register',
'POST /auth/refresh': 'auth.refresh',
'GET /auth/me': 'auth.me',
// Greeter service
'GET /hello': 'greeter.hello',
'GET /welcome/:name': 'greeter.welcome',
// Database service
'REST /users': 'users',
// Health check
'GET /health': 'api.health',
'GET /ready': 'api.ready'
},
// Route-level middlewares
onBeforeCall(ctx: Context<any, Meta>, route: any, req: IncomingMessage, res: ServerResponse) {
ctx.meta.requestTime = Date.now();
},
onAfterCall(ctx: Context<any, Meta>, route: any, req: IncomingMessage, res: ServerResponse, data: any) {
const duration = Date.now() - (ctx.meta.requestTime || 0);
res.setHeader('X-Response-Time', duration + 'ms');
return data;
},
// Error handler
onError(req: IncomingMessage, res: ServerResponse, err: any) {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
res.writeHead(err.code || 500);
res.end(JSON.stringify({
error: {
message: err.message,
code: err.code || 500,
type: err.type || 'UNKNOWN_ERROR'
}
}));
}
}
],
// Do not log client side errors (like 404)
log4XXResponses: false,
// Logging request parameters and response data
logRequestParams: 'info',
logResponseData: 'info',
// Serve assets
assets: {
folder: 'public',
options: {}
}
},
methods: {
/**
* Authenticate the request
*/
async authenticate(ctx: Context, route: any, req: IncomingMessage): Promise<any> {
const auth = req.headers.authorization;
if (auth && auth.startsWith('Bearer ')) {
const token = auth.slice(7);
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'secret') as any;
if (decoded.id) {
const user = await ctx.call('users.get', { id: decoded.id });
if (user) {
return { id: user.id, email: user.email, role: user.role };
}
}
} catch (err) {
throw new Error('Invalid token');
}
}
throw new Error('No token provided');
},
/**
* Authorize the request
*/
async authorize(ctx: Context<any, Meta>, route: any, req: IncomingMessage): Promise<any> {
const user = ctx.meta.user;
if (!user) {
throw new Error('Unauthorized');
}
// Add role-based authorization logic here
return ctx;
}
},
actions: {
health: {
rest: 'GET /health',
handler(ctx: Context) {
return {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
memory: process.memoryUsage(),
nodeID: this.broker.nodeID
};
}
},
ready: {
rest: 'GET /ready',
async handler(ctx: Context) {
const services = await ctx.call('$node.services');
return {
status: 'ready',
services: services.length,
timestamp: new Date().toISOString()
};
}
}
}
});
}
}`,
'src/services/greeter.service.ts': `import { Service, ServiceBroker, Context } from 'moleculer';
interface HelloParams {
name?: string;
}
interface WelcomeParams {
name: string;
}
export default class GreeterService extends Service {
public constructor(broker: ServiceBroker) {
super(broker);
this.parseServiceSchema({
name: 'greeter',
settings: {
upperCase: true
},
dependencies: [],
actions: {
/**
* Say hello
*/
hello: {
rest: 'GET /hello',
cache: {
keys: ['name'],
ttl: 30
},
params: {
name: { type: 'string', optional: true }
},
async handler(ctx: Context<HelloParams>) {
const response = await this.sayHello(ctx.params.name);
// Emit event
ctx.emit('greeter.hello.called', { name: ctx.params.name });
return response;
}
},
/**
* Welcome a user
*/
welcome: {
rest: 'GET /welcome/:name',
params: {
name: 'string'
},
async handler(ctx: Context<WelcomeParams>) {
const user = await ctx.call('users.find', {
query: { email: ctx.params.name }
});
if (user && user.length > 0) {
return this.welcomeUser(user[0]);
}
return this.welcomeGuest(ctx.params.name);
}
}
},
events: {
'user.created': {
async handler(ctx: Context<{ user: any }>) {
this.logger.info('New user created:', ctx.params.user.email);
// Send welcome email
await ctx.call('mail.send', {
to: ctx.params.user.email,
subject: 'Welcome!',
template: 'welcome',
data: {
name: ctx.params.user.name
}
});
}
}
},
methods: {
sayHello(name?: string) {
const greeting = \`Hello \${name || 'Anonymous'}!\`;
if (this.settings.upperCase) {
return greeting.toUpperCase();
}
return greeting;
},
welcomeUser(user: any) {
return {
message: \`Welcome back, \${user.name}!\`,
lastLogin: user.lastLogin,
role: user.role
};
},
welcomeGuest(name: string) {
return {
message: \`Welcome, \${name}! Please register for full access.\`,
guest: true
};
}
},
created() {
this.logger.info('Greeter service created');
},
started() {
this.logger.info('Greeter service started');
},
stopped() {
this.logger.info('Greeter service stopped');
}
});
}
}`,
'src/services/users.service.ts': `import { Service, ServiceBroker } from 'moleculer';
import DbService from 'moleculer-db';
import MongoAdapter from 'moleculer-db-adapter-mongo';
import { Context } from 'moleculer';
import bcrypt from 'bcryptjs';
interface User {
_id?: string;
name: string;
email: string;
password: string;
role: string;
active: boolean;
createdAt: Date;
updatedAt: Date;
lastLogin?: Date;
}
export default class UsersService extends Service {
public constructor(broker: ServiceBroker) {
super(broker);
this.parseServiceSchema({
name: 'users',
mixins: [DbService],
adapter: new MongoAdapter(process.env.MONGO_URI || 'mongodb://localhost:27017/moleculer-db'),
collection: 'users',
settings: {
fields: ['_id', 'name', 'email', 'role', 'active', 'createdAt', 'updatedAt', 'lastLogin'],
entityValidator: {
name: 'string|min:3',
email: 'email',
password: 'string|min:6',
role: { type: 'enum', values: ['user', 'admin'], default: 'user' },
active: { type: 'boolean', default: true }
}
},
hooks: {
before: {
create: [
async function(ctx: Context<{ params: User }>) {
const user = ctx.params;
// Check if email already exists
const found = await this.adapter.findOne({ email: user.email });
if (found) {
throw new Error('Email already exists');
}
// Hash password
user.password = await bcrypt.hash(user.password, 10);
user.createdAt = new Date();
user.updatedAt = new Date();
}
],
update: [
async function(ctx: Context<{ params: User }>) {
const user = ctx.params;
// Hash password if changed
if (user.password) {
user.password = await bcrypt.hash(user.password, 10);
}
user.updatedAt = new Date();
}
]
},
after: {
create: [
async function(ctx: Context<{ params: User }, { user: User }>) {
const user = ctx.result;
// Emit user created event
await ctx.emit('user.created', { user });
// Remove password from response
delete user.password;
return user;
}
],
find: [
async function(ctx: Context<any, { rows: User[] }>) {
// Remove passwords from response
ctx.result.forEach((user: User) => delete user.password);
return ctx.result;
}
],
get: [
async function(ctx: Context<any, { user: User }>) {
// Remove password from response
if (ctx.result) {
delete ctx.result.password;
}
return ctx.result;
}
]
}
},
actions: {
/**
* Get current user
*/
me: {
auth: true,
cache: {
keys: ['#user.id'],
ttl: 60
},
async handler(ctx: Context<any, { user: { id: string } }>) {
const user = await this.getById(ctx.meta.user.id);
if (!user) {
throw new Error('User not found');
}
delete user.password;
return user;
}
},
/**
* Update user profile
*/
updateProfile: {
auth: true,
params: {
name: { type: 'string', optional: true },
email: { type: 'email', optional: true }
},
async handler(ctx: Context<{ name?: string; email?: string }, { user: { id: string } }>) {
const updates: any = {};
if (ctx.params.name) updates.name = ctx.params.name;
if (ctx.params.email) updates.email = ctx.params.email;
updates.updatedAt = new Date();
const user = await this.adapter.updateById(ctx.meta.user.id, { $set: updates });
delete user.password;
return user;
}
},
/**
* Verify user credentials
*/
verifyCredentials: {
params: {
email: 'email',
password: 'string'
},
async handler(ctx: Context<{ email: string; password: string }>) {
const user = await this.adapter.findOne({ email: ctx.params.email });
if (!user || !user.active) {
throw new Error('Invalid credentials');
}
const valid = await bcrypt.compare(ctx.params.password, user.password);
if (!valid) {
throw new Error('Invalid credentials');
}
// Update last login
await this.adapter.updateById(user._id, {
$set: { lastLogin: new Date() }
});
delete user.password;
return user;
}
}
},
methods: {
async seedDB() {
const count = await this.adapter.count();
if (count === 0) {
this.logger.info('Seeding users database...');
await this.adapter.insertMany([
{
name: 'Admin User',
email: 'admin@example.com',
password: await bcrypt.hash('admin123', 10),
role: 'admin',
active: true,
createdAt: new Date(),
updatedAt: new Date()
},
{
name: 'Test User',
email: 'user@example.com',
password: await bcrypt.hash('user123', 10),
role: 'user',
active: true,
createdAt: new Date(),
updatedAt: new Date()
}
]);
this.logger.info('Database seeded!');
}
}
},
async afterConnected() {
// Create indexes
await this.adapter.collection.createIndex({ email: 1 }, { unique: true });
// Seed database
await this.seedDB();
}
});
}
}`,
'src/services/auth.service.ts': `import { Service, ServiceBroker, Context, Errors } from 'moleculer';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
const { MoleculerError } = Errors;
interface LoginParams {
email: string;
password: string;
}
interface RegisterParams {
name: string;
email: string;
password: string;
}
interface RefreshParams {
refreshToken: string;
}
export default class AuthService extends Service {
public constructor(broker: ServiceBroker) {
super(broker);
this.parseServiceSchema({
name: 'auth',
settings: {
jwt: {
secret: process.env.JWT_SECRET || 'moleculer-secret',
expiresIn: process.env.JWT_EXPIRY || '7d'
},
refreshToken: {
secret: process.env.REFRESH_SECRET || 'moleculer-refresh-secret',
expiresIn: '30d'
}
},
actions: {
/**
* Login with email and password
*/
login: {
rest: 'POST /login',
params: {
email: 'email',
password: 'string'
},
async handler(ctx: Context<LoginParams>) {
const { email, password } = ctx.params;
// Verify credentials
const user = await ctx.call('users.verifyCredentials', { email, password });
if (!user) {
throw new MoleculerError('Invalid credentials', 401, 'INVALID_CREDENTIALS');
}
// Generate tokens
const tokens = this.generateTokens(user);
// Log login event
await ctx.emit('user.logged-in', { user, timestamp: new Date() });
return {
user,
...tokens
};
}
},
/**
* Register new user
*/
register: {
rest: 'POST /register',
params: {
name: 'string|min:3',
email: 'email',
password: 'string|min:6'
},
async handler(ctx: Context<RegisterParams>) {
const { name, email, password } = ctx.params;
// Create user
const user = await ctx.call('users.create', {
name,
email,
password,
role: 'user',
active: true
});
// Generate tokens
const tokens = this.generateTokens(user);
// Log registration event
await ctx.emit('user.registered', { user, timestamp: new Date() });
return {
user,
...tokens
};
}
},
/**
* Refresh access token
*/
refresh: {
rest: 'POST /refresh',
params: {
refreshToken: 'string'
},
async handler(ctx: Context<RefreshParams>) {
const { refreshToken } = ctx.params;
try {
// Verify refresh token
const decoded = jwt.verify(
refreshToken,
this.settings.refreshToken.secret
) as any;
// Get user
const user = await ctx.call('users.get', { id: decoded.id });
if (!user) {
throw new Error('User not found');
}
// Generate new tokens
const tokens = this.generateTokens(user);
return tokens;
} catch (err) {
throw new MoleculerError('Invalid refresh token', 401, 'INVALID_TOKEN');
}
}
},
/**
* Get current user info
*/
me: {
auth: true,
async handler(ctx: Context<any, { user: { id: string } }>) {
const user = await ctx.call('users.get', { id: ctx.meta.user.id });
if (!user) {
throw new MoleculerError('User not found', 404, 'USER_NOT_FOUND');
}
return user;
}
},
/**
* Verify JWT token
*/
verify: {
params: {
token: 'string'
},
async handler(ctx: Context<{ token: string }>) {
try {
const decoded = jwt.verify(ctx.params.token, this.settings.jwt.secret) as any;
const user = await ctx.call('users.get', { id: decoded.id });
if (!user) {
throw new Error('User not found');
}
return {
valid: true,
user
};
} catch (err) {
return {
valid: false,
error: err.message
};
}
}
},
/**
* Logout (invalidate tokens)
*/
logout: {
auth: true,
async handler(ctx: Context<any, { user: { id: string } }>) {
// In a real application, you would invalidate the token here
// For example, by adding it to a blacklist in Redis
await ctx.emit('user.logged-out', {
userId: ctx.meta.user.id,
timestamp: new Date()
});
return { success: true };
}
}
},
methods: {
/**
* Generate JWT and refresh tokens
*/
generateTokens(user: any) {
const payload = {
id: user._id || user.id,
email: user.email,
role: user.role
};
const accessToken = jwt.sign(
payload,
this.settings.jwt.secret,
{ expiresIn: this.settings.jwt.expiresIn }
);
const refreshToken = jwt.sign(
payload,
this.settings.refreshToken.secret,
{ expiresIn: this.settings.refreshToken.expiresIn }
);
return {
accessToken,
refreshToken,
expiresIn: this.settings.jwt.expiresIn
};
}
}
});
}
}`,
'src/services/db.service.ts': `import { Service, ServiceBroker } from 'moleculer';
import DbService from 'moleculer-db';
import SequelizeAdapter from 'moleculer-db-adapter-sequelize';
import { Sequelize, DataTypes } from 'sequelize';
export default class DatabaseService extends Service {
public constructor(broker: ServiceBroker) {
super(broker);
const sequelize = new Sequelize({
dialect: 'postgres',
host: process.env.POSTGRES_HOST || 'localhost',
port: parseInt(process.env.POSTGRES_PORT || '5432'),
database: process.env.POSTGRES_DB || 'moleculer',
username: process.env.POSTGRES_USER || 'postgres',
password: process.env.POSTGRES_PASSWORD || 'postgres',
logging: false
});
this.parseServiceSchema({
name: 'products',
mixins: [DbService],
adapter: new SequelizeAdapter(sequelize),
model: {
name: 'product',
define: {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true
},
name: {
type: DataTypes.STRING,
allowNull: false
},
description: {
type: DataTypes.TEXT,
allowNull: true
},
price: {
type: DataTypes.DECIMAL(10, 2),
allowNull: false
},
stock: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
category: {
type: DataTypes.STRING,
allowNull: false
},
active: {
type: DataTypes.BOOLEAN,
defaultValue: true
}
},
options: {
timestamps: true
}
},
settings: {
fields: ['id', 'name', 'description', 'price', 'stock', 'category', 'active', 'createdAt', 'updatedAt'],
entityValidator: {
name: 'string|min:3',
description: { type: 'string', optional: true },
price: 'number|positive',
stock: 'number|integer|min:0',
category: 'string',
active: { type: 'boolean', optional: true }
}
},
actions: {
/**
* Get products by category
*/
byCategory: {
cache: {
keys: ['category'],
ttl: 300
},
params: {
category: 'string'
},
async handler(ctx) {
return this.adapter.find({
where: {
category: ctx.params.category,
active: true
},
order: [['name', 'ASC']]
});
}
},
/**
* Search products
*/
search: {
params: {
query: 'string',
limit: { type: 'number', optional: true, default: 10 }
},
async handler(ctx) {
const { Op } = this.adapter.db.Sequelize;
return this.adapter.find({
where: {
[Op.or]: [
{ name: { [Op.iLike]: \`%\${ctx.params.query}%\` } },
{ description: { [Op.iLike]: \`%\${ctx.params.query}%\` } }
],
active: true
},
limit: ctx.params.limit,
order: [['name', 'ASC']]
});
}
},
/**
* Update stock
*/
updateStock: {
params: {
id: 'string',
quantity: 'number|integer'
},
async handler(ctx) {
const product = await this.adapter.findById(ctx.params.id);
if (!product) {
throw new Error('Product not found');
}
const newStock = product.stock + ctx.params.quantity;
if (newStock < 0) {
throw new Error('Insufficient stock');
}
await this.adapter.updateById(ctx.params.id, {
stock: newStock
});
// Emit stock update event
await ctx.emit('product.stock-updated', {
productId: ctx.params.id,
oldStock: product.stock,
newStock,
change: ctx.params.quantity
});
return this.adapter.findById(ctx.params.id);
}
}
},
methods: {
async seedDB() {
const count = await this.adapter.count();
if (count === 0) {
this.logger.info('Seeding products database...');
const products = [
{
name: 'Laptop Pro',
description: 'High-performance laptop for professionals',
price: 1299.99,
stock: 50,
category: 'Electronics'
},
{
name: 'Wireless Mouse',
description: 'Ergonomic wireless mouse with precision tracking',
price: 49.99,
stock: 200,
category: 'Electronics'
},
{
name: 'USB-C Hub',
description: 'Multi-port USB-C hub with HDMI and ethernet',
price: 79.99,
stock: 150,
category: 'Accessories'
},
{
name: 'Mechanical Keyboard',
description: 'RGB mechanical keyboard with custom switches',
price: 149.99,
stock: 75,
category: 'Electronics'
},
{
name: 'Monitor Stand',
description: 'Adjustable monitor stand with cable management',
price: 89.99,
stock: 100,
category: 'Accessories'
}
];
await this.adapter.insertMany(products);
this.logger.info('Products database seeded!');
}
}
},
async afterConnected() {
// Sync database
await this.adapter.db.sync();
// Seed database
await this.seedDB();
}
});
}
}`,
'src/middlewares/metrics.middleware.ts': `import { Middleware } from 'moleculer';
const MetricsMiddleware: Middleware = {
name: 'MetricsMiddleware',
// Wrap local action handlers
localAction(handler, action) {
return async function metricsHandler(ctx) {
const startTime = Date.now();
try {
const result = await handler(ctx);
// Record success metric
ctx.broker.metrics.increment('moleculer.request.total', {
action: action.name,
service: ctx.service?.name,
status: 'success'
});
// Record duration
const duration = Date.now() - startTime;
ctx.broker.metrics.histogram('moleculer.request.duration', duration, {
action: action.name,
service: ctx.service?.name
});
return result;
} catch (err) {
// Record error metric
ctx.broker.metrics.increment('moleculer.request.total', {
action: action.name,
service: ctx.service?.name,
status: 'error',
error: err.name
});
throw err;
}
};
},
// Wrap broker.call method
call(handler) {
return async function metricsCall(actionName, params, opts) {
const startTime = Date.now();
try {
const result = await handler(actionName, params, opts);
// Record duration
const duration = Date.now() - startTime;
this.metrics.histogram('moleculer.call.duration', duration, {
action: actionName
});
return result;
} catch (err) {
// Record error
this.metrics.increment('moleculer.call.error.total', {
action: actionName,
error: err.name
});
throw err;
}
};
}
};
export default MetricsMiddleware;`,
'docker-compose.yml': `version: '3.8'
services:
# API Gateway
api:
build: .
image: moleculer-microservices
environment:
NODE_ENV: production
SERVICES: api
PORT: 3000
depends_on:
- nats
- redis
- mongo
- postgres
ports:
- "3000:3000"
networks:
- moleculer-net
restart: unless-stopped
# Greeter service
greeter:
build: .
image: moleculer-microservices
environment:
NODE_ENV: production
SERVICES: greeter
depends_on:
- nats
- redis
networks:
- moleculer-net
restart: unless-stopped
# Users service
users:
build: .
image: moleculer-microservices
environment:
NODE_ENV: production
SERVICES: users
depends_on:
- nats
- redis
- mongo
networks:
- moleculer-net
restart: unless-stopped
# Auth service
auth:
build: .
image: moleculer-microservices
environment:
NODE_ENV: production
SERVICES: auth
depends_on:
- nats
- redis
networks:
- moleculer-net
restart: unless-stopped
# Products service
products:
build: .
image: moleculer-microservices
environment:
NODE_ENV: production
SERVICES: products
depends_on:
- nats
- redis
- postgres
networks:
- moleculer-net
restart: unless-stopped
# NATS
nats:
image: nats:2.10-alpine
ports:
- "4222:4222"
- "8222:8222"
networks:
- moleculer-net
restart: unless-stopped
# Redis
redis:
image: redis:7-alpine
ports:
- "6379:6379"
networks:
- moleculer-net
restart: unless-stopped
# MongoDB
mongo:
image: mongo:7
environment:
MONGO_INITDB_DATABASE: moleculer-db
ports:
- "27017:27017"
volumes:
- mongo-data:/data/db
networks:
- moleculer-net
restart: unless-stopped
# PostgreSQL
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: moleculer
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- moleculer-net
restart: unless-stopped
# Jaeger
jaeger:
image: jaegertracing/all-in-one:1.52
environment:
COLLECTOR_ZIPKIN_HOST_PORT: ":9411"
ports:
- "5775:5775/udp"
- "6831:6831/udp"
- "6832:6832/udp"
- "5778:5778"
- "16686:16686"
- "14268:14268"
- "14250:14250"
- "9411:9411"
networks:
- moleculer-net
restart: unless-stopped
# Prometheus
prometheus:
image: prom/prometheus:v2.48.1
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
ports:
- "9090:9090"
networks:
- moleculer-net
restart: unless-stopped
# Grafana
grafana:
image: grafana/grafana:10.2.3
environment:
GF_SECURITY_ADMIN_PASSWORD: admin
ports:
- "3001:3000"
volumes:
- grafana-data:/var/lib/grafana
networks:
- moleculer-net
restart: unless-stopped
networks:
moleculer-net:
driver: bridge
volumes:
mongo-data:
postgres-data:
prometheus-data:
grafana-data:`,
'Dockerfile': `FROM node:20-alpine
# Install build dependencies
RUN apk add --no-cache python3 make g++
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY tsconfig.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY src ./src
# Build TypeScript
RUN npm run build
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
USER nodejs
# Start the service
CMD ["node", "dist/index.js"]`,
'prometheus.yml': `global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'moleculer'
static_configs:
- targets:
- 'api:3030'
- 'greeter:3030'
- 'users:3030'
- 'auth:3030'
- 'products:3030'
metrics_path: '/metrics'`,
'jest.config.js': `module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src', '<rootDir>/test'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/*.interface.ts'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};`,
'test/unit/greeter.spec.ts': `import { ServiceBroker } from 'moleculer';
import GreeterService from '../../src/services/greeter.service';
describe('Test Greeter service', () => {
let broker: ServiceBroker;
let service: any;
beforeAll(() => {
broker = new ServiceBroker({ logger: false });
service = broker.createService(GreeterService);
return broker.start();
});
afterAll(() => broker.stop());
describe('Test greeter.hello action', () => {
it('should return with Hello Anonymous', async () => {
const result = await broker.call('greeter.hello');
expect(result).toBe('HELLO ANONYMOUS!');
});
it('should return with Hello John', async () => {
const result = await broker.call('greeter.hello', { name: 'John' });
expect(result).toBe('HELLO JOHN!');
});
});
describe('Test greeter.welcome action', () => {
it('should welcome a guest', async () => {
const result = await broker.call('greeter.welcome', { name: 'John' });
expect(result).toHaveProperty('message');
expect(result).toHaveProperty('guest', true);
expect(result.message).toContain('Welcome, John!');
});
});
});`,
'test/integration/api.spec.ts': `import { ServiceBroker } from 'moleculer';
import ApiService from '../../src/services/api.service';
import request from 'supertest';
describe('Test API Gateway', () => {
let broker: ServiceBroker;
let server: any;
beforeAll(async () => {
broker = new ServiceBroker({
logger: false,
transporter: null
});
broker.createService(ApiService);
await broker.start();
server = (broker.getLocalService('api') as any).server;
});
afterAll(() => broker.stop());
describe('Test health endpoints', () => {
it('GET /api/health should return health status', async () => {
const res = await request(server)
.get('/api/health')
.expect(200);
expect(res.body).toHaveProperty('status', 'ok');
expect(res.body).toHaveProperty('timestamp');
expect(res.body).toHaveProperty('uptime');
expect(res.body).toHaveProperty('memory');
});
it('GET /api/ready should return ready status', async () => {
const res = await request(server)
.get('/api/ready')
.expect(200);
expect(res.body).toHaveProperty('status', 'ready');
expect(res.body).toHaveProperty('services');
expect(res.body).toHaveProperty('timestamp');
});
});
describe('Test CORS', () => {
it('should have CORS headers', async () => {
const res = await request(server)
.get('/api/health')
.expect(200);
expect(res.headers).toHaveProperty('access-control-allow-origin', '*');
});
});
});`,
'.eslintrc.js': `module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
project: './tsconfig.json'
},
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended'
],
env: {
node: true,
jest: true
},
rules: {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'no-console': ['warn', { allow: ['warn', 'error'] }]
}
};`,
'kubernetes/namespace.yaml': `apiVersion: v1
kind: Namespace
metadata:
name: moleculer-microservices
labels:
name: moleculer-microservices`,
'kubernetes/api-deployment.yaml': `apiVersion: apps/v1
kind: Deployment
metadata:
name: api-gateway
namespace: moleculer-microservices
labels:
app: api-gateway
spec:
replicas: 2
selector:
matchLabels:
app: api-gateway
template:
metadata:
labels:
app: api-gateway
spec:
containers:
- name: api
image: moleculer-microservices:latest
imagePullPolicy: IfNotPresent
env:
- name: NODE_ENV
value: "production"
- name: SERVICES
value: "api"
- name: TRANSPORTER
value: "nats://nats:4222"
- name: CACHER
value: "redis://redis:6379"
ports:
- containerPort: 3000
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /api/health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /api/ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: api-gateway
namespace: moleculer-microservices
spec:
selector:
app: api-gateway
ports:
- port: 3000
targetPort: 3000
type: LoadBalancer`,
'README.md': `# Moleculer Microservices
A production-ready microservices architecture built with Moleculer framework, featuring TypeScript support, service discovery, and comprehensive monitoring.
## Features
- **Service-Oriented Architecture**: Built-in service broker with automatic service discovery
- **Multiple Transport Layers**: NATS, Redis, TCP, and more
- **Fault Tolerance**: Circuit breaker, retry logic, timeout, and bulkhead patterns
- **Caching**: Redis-based caching with automatic cache invalidation
- **Load Balancing**: Multiple strategies (RoundRobin, Random, CPU usage-based)
- **Authentication & Authorization**: JWT-based auth with role-based access control
- **Database Integration**: MongoDB and PostgreSQL with ORM support
- **API Gateway**: RESTful endpoints with rate limiting and CORS
- **Monitoring**: Prometheus metrics and Jaeger distributed tracing
- **Hot Reload**: Development mode with automatic service reloading
- **Testing**: Comprehensive unit and integration tests with Jest
- **Docker & Kubernetes**: Production-ready containerization
## Quick Start
1. Install dependencies:
\`\`\`bash
npm install
\`\`\`
2. Set up environment variables:
\`\`\`bash
cp .env.example .env
\`\`\`
3. Start infrastructure services:
\`\`\`bash
docker-compose up -d nats redis mongo postgres jaeger
\`\`\`
4. Run in development mode:
\`\`\`bash
npm run dev
\`\`\`
5. Or run specific services:
\`\`\`bash
npm run start:services
\`\`\`
## Architecture
### Services
- **API Gateway**: REST API endpoints, authentication, rate limiting
- **Auth Service**: JWT authentication and authorization
- **Users Service**: User management with MongoDB
- **Products Service**: Product catalog with PostgreSQL
- **Greeter Service**: Example service demonstrating patterns
### Infrastructure
- **NATS**: Message broker for inter-service communication
- **Redis**: Caching and session storage
- **MongoDB**: Document database for users
- **PostgreSQL**: Relational database for products
- **Jaeger**: Distributed tracing
- **Prometheus**: Metrics collection
- **Grafana**: Metrics visualization
## API Endpoints
### Authentication
- \`POST /api/auth/login\` - User login
- \`POST /api/auth/register\` - User registration
- \`POST /api/auth/refresh\` - Refresh access token
- \`GET /api/auth/me\` - Get current user
### Users
- \`GET /api/users\` - List users (admin only)
- \`GET /api/users/:id\` - Get user by ID
- \`PUT /api/users/:id\` - Update user
- \`DELETE /api/users/:id\` - Delete user
### Products
- \`GET /api/products\` - List products
- \`POST /api/products\` - Create product
- \`GET /api/products/:id\` - Get product
- \`PUT /api/products/:id\` - Update product
- \`DELETE /api/products/:id\` - Delete product
### Health
- \`GET /api/health\` - Health check
- \`GET /api/ready\` - Readiness check
## Development
### Running Tests
\`\`\`bash
npm test # Run all tests
npm run test:watch # Run tests in watch mode
npm run test:coverage # Run tests with coverage
\`\`\`
### Building for Production
\`\`\`bash
npm run build
\`\`\`
### Docker Deployment
\`\`\`bash
docker-compose up -d # Start all services
docker-compose logs -f # View logs
docker-compose down # Stop all services
\`\`\`
### Kubern