PantheraHive Workflow Execution: Microservice Scaffolder (Step 1 of 2: Generate)
The "Microservice Scaffolder" workflow has successfully generated the comprehensive structure and initial files for your "Test Service Name" microservice, leveraging Node.js (Express) with MongoDB, Docker, and Kubernetes for deployment.
The following directory and file structure has been generated for your microservice:
*Note: Ingress requires an Ingress Controller (e.g., Nginx Ingress) to be installed in your cluster.*
## CI/CD
The `.github/workflows/` directory contains GitHub Actions workflows:
- `build-and-test.yml`: Triggers on push to `main` and pull requests. It builds the Docker image and runs tests.
- `deploy-k8s.yml`: Triggers on push to `main` after `build-and-test.yml` passes. It pushes the Docker image to a registry and applies Kubernetes manifests.
*You will need to configure GitHub Secrets for your Docker registry credentials and Kubernetes access.*
## Project Structure
- `src/`: Contains the core application logic.
- `app.js`: Express application setup.
- `server.js`: Application entry point.
- `config/`: Environment-specific configurations and database connection.
- `models/`: Mongoose schemas.
- `routes/`: API route definitions.
- `controllers/`: Request handlers for routes.
- `middleware/`: Custom Express middleware (e.g., error handling).
- `utils/`: Utility functions and custom error classes.
- `tests/`: Unit and integration tests.
- `docker/`: Docker Compose configuration for local development.
- `k8s/`: Kubernetes deployment manifests.
- `.github/workflows/`: GitHub Actions CI/CD pipelines.
## Environment Variables
The service relies on environment variables for configuration. See `.env.example` for a list of required variables.
## Linting
This project uses ESLint for code linting.
To lint your code:
javascript
const request = require('supertest');
const app = require('../../src/app');
const TestItem = require('../../src/models/TestItem');
describe('TestItem API Endpoints', () => {
// Test GET all items
it('should get all test items', async () => {
await TestItem.create({ name: 'Item 1', description: 'Desc 1' });
await TestItem.create({ name: 'Item 2', description: 'Desc 2' });
const res = await request(app).get('/api/v1/test-items');
expect(res.statusCode).toEqual(200);
expect(res.body.status).toBe('success');
expect(res.body.results).toBe(2);
expect(res.body.data.data.length).toBe(2);
The "Microservice Scaffolder" workflow has successfully generated a comprehensive microservice blueprint based on your specifications. Below is the detailed output, including project structure, core files, API routes, database models, testing setup, Dockerization, CI/CD configuration, and Kubernetes deployment scripts.
Your new Node.js (Express) microservice, "Test Service Name," is ready! This output provides a complete foundation for development, including API structure, MongoDB integration, testing, Docker setup, and Kubernetes deployment configurations.
.
├── .github/
│ └── workflows/
│ └── main.yml
├── k8s/
│ ├── configmap.yaml
│ ├── deployment.yaml
│ ├── ingress.yaml
│ └── service.yaml
├── src/
│ ├── config/
│ │ ├── db.js
│ │ └── index.js
│ ├── controllers/
│ │ └── example.controller.js
│ ├── middlewares/
│ │ ├── auth.middleware.js
│ │ └── error.middleware.js
│ ├── models/
│ │ └── example.model.js
│ ├── routes/
│ │ └── api/
│ │ └── v1/
│ │ ├── example.routes.js
│ │ └── health.routes.js
│ ├── services/
│ │ └── example.service.js
│ ├── tests/
│ │ ├── integration/
│ │ │ └── example.api.test.js
│ │ └── unit/
│ │ └── example.service.test.js
│ ├── utils/
│ │ └── logger.js
│ └── app.js
├── .dockerignore
├── .env.example
├── .gitignore
├── Dockerfile
├── docker-compose.yml
├── jest.config.js
├── Makefile
├── package.json
├── README.md
└── server.js
package.json
{
"name": "test-service-name",
"version": "1.0.0",
"description": "A microservice for Test Service Name",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "cross-env NODE_ENV=development nodemon server.js",
"test": "cross-env NODE_ENV=test jest --detectOpenHandles --forceExit",
"test:watch": "cross-env NODE_ENV=test jest --watchAll",
"lint": "eslint .",
"lint:fix": "eslint . --fix"
},
"keywords": [
"microservice",
"node",
"express",
"mongodb"
],
"author": "PantheraHive AI Assistant",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"helmet": "^7.1.0",
"mongoose": "^8.4.1",
"morgan": "^1.10.0",
"winston": "^3.13.0"
},
"devDependencies": {
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.7.0",
"nodemon": "^3.1.3",
"prettier": "^3.3.1",
"supertest": "^7.0.0"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": [
"/node_modules/"
]
}
}
.env.example
# Application Configuration
PORT=3000
NODE_ENV=development # development, production, test
# MongoDB Configuration
MONGODB_URI=mongodb://localhost:27017/test_service_db_dev
MONGODB_URI_TEST=mongodb://localhost:27017/test_service_db_test
# JWT Secret (for authentication, if implemented)
JWT_SECRET=your_super_secret_jwt_key
JWT_EXPIRES_IN=1h
# Other example configurations
SERVICE_NAME="Test Service Name"
LOG_LEVEL=info # debug, info, warn, error
server.js
require('dotenv').config();
const app = require('./src/app');
const connectDB = require('./src/config/db');
const logger = require('./src/utils/logger');
const config = require('./src/config');
const PORT = config.port;
// Connect to MongoDB
connectDB();
// Start the server
const server = app.listen(PORT, () => {
logger.info(`Server running on port ${PORT} in ${config.env} mode`);
logger.info(`Access API at http://localhost:${PORT}/api/v1`);
});
// Handle unhandled promise rejections
process.on('unhandledRejection', (err) => {
logger.error('UNHANDLED REJECTION! 💥 Shutting down...');
logger.error(err.name, err.message);
server.close(() => {
process.exit(1);
});
});
// Handle uncaught exceptions
process.on('uncaughtException', (err) => {
logger.error('UNCAUGHT EXCEPTION! 💥 Shutting down...');
logger.error(err.name, err.message);
process.exit(1);
});
src/app.js
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const morgan = require('morgan');
const config = require('./config');
const { errorHandler } = require('./middlewares/error.middleware');
const apiV1Routes = require('./routes/api/v1'); // Assuming an index.js in v1 folder
const logger = require('./utils/logger');
const app = express();
// Security Middleware
app.use(helmet());
// CORS
app.use(cors());
// Request logging (Morgan)
if (config.env === 'development') {
app.use(morgan('dev'));
} else {
app.use(morgan('combined', { stream: { write: message => logger.info(message.trim()) } }));
}
// Body parser
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// API V1 Routes
app.use('/api/v1', apiV1Routes);
// Catch-all for undefined routes
app.use((req, res, next) => {
res.status(404).json({
status: 'fail',
message: `Can't find ${req.originalUrl} on this server!`,
});
});
// Global Error Handling Middleware
app.use(errorHandler);
module.exports = app;
src/config/index.js
const dotenv = require('dotenv');
dotenv.config();
module.exports = {
env: process.env.NODE_ENV || 'development',
port: process.env.PORT || 3000,
mongodb: {
uri: process.env.NODE_ENV === 'test' ? process.env.MONGODB_URI_TEST : process.env.MONGODB_URI,
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN,
},
serviceName: process.env.SERVICE_NAME || 'Test Service Name',
logLevel: process.env.LOG_LEVEL || 'info',
};
src/utils/logger.js
const winston = require('winston');
const config = require('../config');
const enumerateErrorFormat = winston.format((info) => {
if (info instanceof Error) {
Object.assign(info, { message: info.stack });
}
return info;
});
const logger = winston.createLogger({
level: config.logLevel,
format: winston.format.combine(
enumerateErrorFormat(),
config.env === 'development' ? winston.format.colorize() : winston.format.uncolorize(),
winston.format.splat(),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.printf(({ level, message, timestamp }) => `${timestamp} ${level}: ${message}`)
),
transports: [
new winston.transports.Console({
stderrLevels: ['error'],
}),
],
});
module.exports = logger;
src/middlewares/error.middleware.js
const logger = require('../utils/logger');
const config = require('../config');
const errorHandler = (err, req, res, next) => {
let { statusCode, message } = err;
if (config.env === 'production' && !err.isOperational) {
statusCode = 500;
message = 'Internal Server Error';
}
res.locals.errorMessage = err.message;
const response = {
code: statusCode,
message,
...(config.env === 'development' && { stack: err.stack }),
};
if (config.env === 'development') {
logger.error(err);
}
res.status(statusCode).send(response);
};
module.exports = {
errorHandler,
};
src/middlewares/auth.middleware.js (Example)
const jwt = require('jsonwebtoken');
const config = require('../config');
const protect = (req, res, next) => {
let token;
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return res.status(401).json({ message: 'Not authorized, no token' });
}
try {
const decoded = jwt.verify(token, config.jwt.secret);
// Attach user to req object if user data is available in the token
// req.user = decoded.id;
next();
} catch (error) {
res.status(401).json({ message: 'Not authorized, token failed' });
}
};
// Example for role-based authorization
const authorize = (...roles) => {
return (req, res, next) => {
// Assuming req.user exists from the 'protect' middleware and has a 'role' property
if (!req.user || !roles.includes(req.user.role)) {
return res.status(403).json({ message: 'Forbidden, insufficient permissions' });
}
next();
};
};
module.exports = { protect, authorize };
src/routes/api/v1/index.js (Entry point for V1 API)
const express = require('express');
const healthRoutes = require('./health.routes');
const exampleRoutes = require('./example.routes');
const router = express.Router();
router.use('/health', healthRoutes);
router.use('/examples', exampleRoutes);
module.exports = router;
src/routes/api/v1/health.routes.js
const express = require('express');
const router = express.Router();
/**
* @route GET /api/v1/health
* @description Health check endpoint
* @access Public
*/
router.get('/', (req, res) => {
res.status(200).json({
status: 'ok',
message: 'Test Service Name is healthy!',
timestamp: new Date().toISOString(),
});
});
module.exports = router;
src/routes/api/v1/example.routes.js
const express = require('express');
const exampleController = require('../../controllers/example.controller');
const { protect } = require('../../middlewares/auth.middleware'); // Assuming authentication is needed
const router = express.Router();
router
.route('/')
.post(protect, exampleController.createExample) // Example: requires authentication
.get(exampleController.getAllExamples); // Example: public access
router
.route('/:id')
.get(exampleController.getExampleById)
.put(protect, exampleController.updateExample)
.delete(protect, exampleController.deleteExample);
module.exports = router;
src/controllers/example.controller.js
const exampleService = require('../services/example.service');
const logger = require('../utils/logger');
// Example: Create a new example
exports.createExample = async (req, res, next) => {
try {
const example = await exampleService.createExample(req.body);
res.status(201).json({
status: 'success',
data: example,
});
} catch (error) {
logger.error('Error creating example:', error);
next(error);
}
};
// Example: Get all examples
exports.getAllExamples = async (req, res, next) => {
try {
const examples = await exampleService.getAllExamples();
res.status(200).json({
status: 'success',
results: examples.length,
data: examples,
});
} catch (error) {
logger.error('Error fetching all examples:', error);
next(error);
}
};
// Example: Get an example by ID
exports.getExampleById = async (req, res, next) => {
try {
const example = await exampleService.getExampleById(req.params.id);
if (!example) {
return res.status(404).json({
status: 'fail',
message: 'Example not found',
});
}
res.status(200).json({
status: 'success',
data: example,
});
} catch (error) {
logger.error(`Error fetching example with ID ${req.params.id}:`, error);
next(error);
}
};
// Example: Update an example
exports.updateExample = async (req, res, next) => {
try {
const example = await exampleService.updateExample(req.params.id, req.body);
if (!example) {
return res.status(404).json({
status: 'fail',
message: 'Example not found',
});
}
res.status(200).json({
status: 'success',
data: example,
});
} catch (error) {
logger.error(`Error updating example with ID ${req.params.id}:`, error);
next(error);
}
};
// Example: Delete an example
exports.deleteExample = async (req, res, next) => {
try {
const example = await exampleService.deleteExample(req.params.id);
if (!example) {
return res.status(404).json({
status: 'fail',
message: 'Example not found',
});
}
res.status(204).json({
status: 'success',
data: null, // No content for 204
});
} catch (error) {
logger.error(`Error deleting example with ID ${req.params.id}:`, error);
next(error);
}
};
src/services/example.service.js
const Example = require('../models/example.model');
// Service function to create a new example
exports.createExample = async (exampleData) => {
const example = new Example(exampleData);
await example.save();
return example;
};
// Service function to get all examples
exports.getAllExamples = async () => {
const examples = await Example.find();
return examples;
};
// Service function to get an example by ID
exports.getExampleById = async (id) => {
const example = await Example.findById(id);
return example;
};
// Service function to update an example
exports.updateExample = async (id, updateData) => {
const example = await Example.findByIdAndUpdate(id, updateData, { new: true, runValidators: true });
return example;
};
// Service function to delete an example
exports.deleteExample = async (id) => {
const example = await Example.findByIdAndDelete(id);
return example;
};
src/config/db.js
const mongoose = require('mongoose');
const config = require('./index');
const logger = require('../utils/logger');
const connectDB = async () => {
try {
await mongoose.connect(config.mongodb.uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
logger.info('MongoDB connected successfully!');
} catch (err) {
logger.error('MongoDB connection error:', err);
process.exit(1); // Exit process with failure
}
};
module.exports = connectDB;
src/models/example.model.js
const mongoose = require('mongoose');
const exampleSchema = new mongoose.Schema(
{
name: {
type: String,
required: [true, 'Example name is required'],
trim: true,
minlength: [3, 'Name must be at least 3 characters long'],
maxlength: [100, 'Name cannot exceed 100 characters'],
},
description: {
type: String,
trim: true,
maxlength: [500, 'Description cannot exceed 500 characters'],
},
status: {
type: String,
enum: ['active', 'inactive', 'pending'],
default: 'active',
},
// Add more fields as needed
},
{
timestamps: true, // Adds createdAt and updatedAt fields
}
);
const Example = mongoose.model('Example', exampleSchema);
module.exports = Example;
jest.config.js
module.exports = {
testEnvironment: 'node',
// Automatically clear mock calls and instances between every test
clearMocks: true,
// The directory where Jest should output its coverage files
coverageDirectory: 'coverage',
// An array of glob patterns that Jest should use to find test files
testMatch: [
'**/src/tests/**/*.test.js',
],
// Ignore coverage for node_modules and test files themselves
coveragePathIgnorePatterns: [
'/node_modules/',
'src/tests/',
],
// Setup file to run before all tests
setupFilesAfterEnv: ['./src/tests/setupTests.js'],
};
src/tests/setupTests.js
// This file can be used for global test setup, e.g., connecting/disconnecting from a test database
const mongoose = require('mongoose');
const config = require('../config');
const logger = require('../utils/logger');
// Ensure MongoDB connection for tests
beforeAll(async () => {
if (mongoose.connection.readyState === 0) {
await mongoose.connect(config.mongodb.uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
logger.info('MongoDB Test DB connected.');
}
});
// Clear the database after each test
afterEach(async () => {
await mongoose.connection.db.dropDatabase();
});
// Disconnect from MongoDB after all tests are done
afterAll(async () => {
await mongoose.disconnect();
logger.info('MongoDB Test DB disconnected.');
});
src/tests/unit/example.service.test.js
const Example = require('../../models/example.model');
const exampleService = require('../../services/example.service');
const mongoose = require('mongoose');
// Mock the Example model to prevent actual database interaction for unit tests
jest.mock('../../models/example.model');
describe('Example Service', () => {
beforeEach(() => {
// Clear all mocks before each test
jest.clearAllMocks();
});
describe('createExample', () => {
it('should create and return a new example', async () => {
const exampleData = { name: 'Test Example', description: 'A description' };
const mockExample = { ...exampleData, _id: new mongoose.Types.ObjectId(), save: jest.fn().mockResolvedValue(true) };
Example.mockImplementation(() => mockExample);
const result = await exampleService.createExample(exampleData);
expect(Example).toHaveBeenCalledWith(exampleData);
expect(mockExample.save).toHaveBeenCalledTimes(1);
expect(result).toEqual(mockExample);
});
});
describe('getAllExamples', () => {
it('should return all examples', async () => {
const mockExamples = [{ name: 'Ex1' }, { name: 'Ex2' }];
Example.find.mockResolvedValue(mockExamples);
const result = await exampleService.getAllExamples();
expect(Example.find).toHaveBeenCalledTimes(1);
expect(result).toEqual(mockExamples);
});
});
// Add more unit tests for getExampleById, updateExample, deleteExample
});
src/tests/integration/example.api.test.js
const request = require('supertest');
const app = require('../../app');
const Example = require('../../models/example.model');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const config = require('../../config');
// Helper to generate a test token (for authenticated routes)
const generateAuthToken = (userId = new mongoose.Types.ObjectId(), role = 'user') => {
return jwt.sign({ id: userId, role }, config.jwt.secret, { expiresIn: '1h' });
};
describe('Example API Integration Tests', () => {
let authToken;
let testExampleId;
beforeAll(async () => {
// Generate a token for authenticated requests
authToken = generateAuthToken();
});
beforeEach(async () => {
// Clean up DB and create a fresh example for each test
await Example.deleteMany({});
const example = await Example.create({ name: 'Initial Example', description: 'Description for initial example' });
testExampleId = example._id;
});
describe('GET /api/v1/examples', () => {
it('should return all examples', async () => {
const res = await request(app).get('/api/v1/examples');
expect(res.statusCode).toEqual(200);
expect(res.body.status).toEqual('success');
expect(res.body.results).toBe(1);
expect(res.body.data[0].name).toEqual('Initial Example');
});
});
describe('POST /api/v1/examples', () => {
it('should create a new example if authenticated', async () => {
const newExample = { name: 'New Example', description: 'A brand new example' };
const res = await request(app)
.post('/api/v1/examples')
.set('Authorization', `Bearer ${authToken}`)
.send(newExample);
expect(res.statusCode).toEqual(201);
expect(res.body.status).toEqual('success');
expect(res.body.data.name).toEqual(newExample.name);
const count = await Example.countDocuments();
expect(count).toBe(2); // Initial + new
});
it('should return 401 if not authenticated', async () => {
const newExample = { name: 'Unauthorized Example' };
const res = await request(app)
.post('/api/v1/examples')
.send(newExample);
expect(res.statusCode).toEqual(401);
expect(res.body.message).toEqual('Not authorized, no token');
});
});
describe('GET /api/v1/examples/:id', () => {
it('should return an example by ID', async () => {
const res = await request(app).get(`/api/v1/examples/${testExampleId}`);
expect(res.statusCode).toEqual(200);
expect(res.body.status).toEqual('success');
expect(res.body.data.name).toEqual('Initial Example');
});
it('should return 404 if example not found', async () => {
const nonExistentId = new mongoose.Types.ObjectId();
const res = await request(app).get(`/api/v1/examples/${nonExistentId}`);
expect(res.statusCode).toEqual(404);
expect(res.body.message).toEqual('Example not found');
});
});
// Add more integration tests for PUT and DELETE
});
.dockerignore
node_modules
npm-debug.log
.env
.git
.gitignore
Dockerfile
docker-compose.yml
README.md
coverage
src/tests
Dockerfile
# Stage 1: Build the application
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package.json and package-lock.json first to leverage Docker cache
COPY package*.json ./
# Install dependencies
RUN npm install --production
# Copy the rest of the application code
COPY . .
# Stage 2: Create the production-ready image
FROM node:20-alpine
WORKDIR /app
# Copy only necessary files from the builder stage
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/server.js ./server.js
COPY --from=builder /app/src ./src
COPY --from=builder /app/.env.example ./.env.example
# Expose the port the app runs on
EXPOSE 3000
# Set environment variables
ENV NODE_ENV=production
ENV PORT=3000
# Command to run the application
CMD ["node", "server.js"]
docker-compose.yml
version: '3.8'
services:
app:
container_name: test-service-name-app
build:
context: .
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
# Load variables from .env file. For production, consider using Kubernetes Secrets or ConfigMaps.
# These are default values, you can override them in .env
MONGODB_URI: mongodb://mongodb:27017/test_service_db_dev
PORT: 3000
NODE_ENV: development
JWT_SECRET: ${JWT_SECRET} # Ensure JWT_SECRET is set in your host's .env file
depends_on:
- mongodb
volumes:
- .:/app
- /app/node_modules # Prevents host's node_modules from overwriting container's
command: npm run dev # Use nodemon for development auto-reloads
mongodb:
container_name: test-service-name-mongodb
image: mongo:latest
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db
environment:
MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME:-admin}
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD:-password}
MONGO_INITDB_DATABASE: test_service_db_dev # Initial database
volumes:
mongodb_data:
.github/workflows/main.yml
name: CI/CD Pipeline
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
- develop
env:
SERVICE_NAME: test-service-name
REGISTRY: ghcr.io # GitHub Container Registry
IMAGE_NAME: ghcr.io/${{ github.repository_owner }}/${{ env.SERVICE_NAME }}
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint
- name: Set
\n