Skip to main content

Adding a New Microservice to Helix Platform

This guide walks through creating and deploying a new microservice in the Helix platform.


Overview

Adding a new service involves:

  1. Creating the service using the template
  2. Configuring the service (ports, environment variables)
  3. Adding to Docker Compose
  4. Updating API Gateway routing
  5. Adding to Terraform (ECR, ECS)
  6. Deploying to environments

Time Estimate: 2-4 hours for a basic service


Step 1: Clone Template Service

Use the template service as your starting point:

# Navigate to apps directory
cd apps

# Clone template
cp -r template-service your-new-service

# Clean up
cd your-new-service
rm -rf dist node_modules

Step 2: Configure Service

2.1 Update project.json

{
"name": "your-new-service",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/your-new-service/src",
"projectType": "application",
"targets": {
"build": {
"executor": "@nx/webpack:webpack",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"target": "node",
"compiler": "tsc",
"outputPath": "dist/apps/your-new-service",
"main": "apps/your-new-service/src/main.ts",
"tsConfig": "apps/your-new-service/tsconfig.app.json",
"assets": ["apps/your-new-service/src/assets"],
"webpackConfig": "apps/your-new-service/webpack.config.js"
}
},
"serve": {
"executor": "@nx/js:node",
"options": {
"buildTarget": "your-new-service:build"
}
}
},
"tags": ["type:service"]
}

2.2 Update README.md

# Your New Service

## Overview
Brief description of what this service does.

## Responsibilities
- Primary function 1
- Primary function 2
- Primary function 3

## Port
- **Port:** 3010 (choose next available port)

## Environment Variables
- `PORT` - Service port (default: 3010)
- `CENTRAL_DATABASE_URL` - Central database connection
- `REDIS_HOST` - Redis host
- `YOUR_API_KEY` - Any service-specific API keys

## API Endpoints
- `GET /health` - Health check
- `GET /api/v1/your-resource` - List resources
- `POST /api/v1/your-resource` - Create resource

## Dependencies
- `@helix/shared/*` - Shared libraries
- Any service-specific npm packages

2.3 Update main.ts

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app/app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);

// Global prefix
app.setGlobalPrefix('api/v1');

// Validation
app.useGlobalPipes(new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}));

// CORS
app.enableCors({
origin: process.env.CORS_ORIGIN?.split(',') || '*',
credentials: true,
});

const port = process.env.PORT || 3010;
await app.listen(port);

console.log(`🚀 Your New Service running on http://localhost:${port}`);
}

bootstrap();

2.4 Choose a Port

Next available ports:

  • 3000: API Gateway
  • 3001: Auth Service
  • 3002: Tenant Service
  • 3003: User Service (not created yet)
  • 3004: KIRA Service
  • 3005: VERA Service
  • 3006: Data Service (not created yet)
  • 3007: Notification Service (not created yet)
  • 3008: Admin Service
  • 3009: File Service
  • 3010: Your New Service ← Use this

Update in:

  • apps/your-new-service/src/main.ts (default port)
  • apps/your-new-service/.env.example

Step 3: Add to Docker Compose

Update docker-compose.yml:

services:
# ... existing services

your-new-service:
build:
context: .
dockerfile: Dockerfile.template
args:
SERVICE_NAME: your-new-service
environment:
NODE_ENV: development
PORT: 3010
SERVICE_NAME: your-new-service
CENTRAL_DATABASE_URL: ${CENTRAL_DATABASE_URL}
WORKOS_API_KEY: ${WORKOS_API_KEY}
WORKOS_CLIENT_ID: ${WORKOS_CLIENT_ID}
REDIS_HOST: redis
REDIS_PORT: 6379
CORS_ORIGIN: ${CORS_ORIGIN:-*}
# Add service-specific env vars
YOUR_API_KEY: ${YOUR_API_KEY:-}
ports:
- '3010:3010'
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
networks:
- helix-network
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3010/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s

Step 4: Update API Gateway Routing

Add routing in apps/api-gateway/src/proxy/setup-proxy.ts:

export function setupProxy(app: INestApplication) {
const httpAdapter = app.getHttpAdapter();
const server = httpAdapter.getInstance();

// ... existing routes

// Your new service
server.use(
'/api/v1/your-resource',
createProxyMiddleware({
target: process.env.YOUR_NEW_SERVICE_URL || 'http://localhost:3010',
changeOrigin: true,
logLevel: 'debug',
onProxyReq: (proxyReq, req) => {
// Add request ID
const requestId = (req as any).id || uuidv4();
proxyReq.setHeader('X-Request-ID', requestId);
},
onError: (err, req, res) => {
console.error('Proxy error for your-new-service:', err);
(res as any).status(502).json({
statusCode: 502,
message: 'Service temporarily unavailable',
service: 'your-new-service',
});
},
})
);
}

Update API Gateway's .env.example:

# Service URLs
YOUR_NEW_SERVICE_URL=http://your-new-service:3010

Update docker-compose.yml for API Gateway:

api-gateway:
# ... existing config
environment:
# ... existing vars
YOUR_NEW_SERVICE_URL: http://your-new-service:3010

Step 5: Add to Terraform

5.1 Add to ECR Module

Update terraform/modules/ecr/main.tf:

locals {
services = [
"api-gateway",
"auth-service",
"tenant-service",
"user-service",
"kira-service",
"vera-service",
"data-service",
"notification-service",
"your-new-service" # ADD HERE
]
}

5.2 Add to ECS Cluster Module

Update terraform/modules/ecs-cluster/main.tf:

locals {
services = {
# ... existing services

your-new-service = {
port = 3010
cpu = var.service_cpu
memory = var.service_memory
desired_count = var.service_desired_count
min_capacity = var.service_min_capacity
max_capacity = var.service_max_capacity
public_facing = false
health_check_path = "/health"
}
}
}

If your service needs specific secrets, update the secrets section in the task definition:

secrets = concat(
# Common secrets for all services
[...],

# Service-specific secrets
each.key == "your-new-service" ? [
{
name = "YOUR_API_KEY"
valueFrom = var.your_api_key_secret_arn
}
] : [],
)

Step 6: Update GitHub Actions

6.1 Add to Test Workflow

Update .github/workflows/test.yml:

docker-build:
strategy:
matrix:
service:
- api-gateway
- auth-service
- tenant-service
- user-service
- kira-service
- vera-service
- data-service
- notification-service
- your-new-service # ADD HERE

6.2 Add to Staging Deployment

Update .github/workflows/deploy-staging.yml:

build-and-push:
strategy:
matrix:
service:
# ... existing services
- your-new-service # ADD HERE

update-ecs-services:
strategy:
matrix:
service:
# ... existing services
- your-new-service # ADD HERE

6.3 Add to Production Deployment

Update .github/workflows/deploy-production.yml:

# Same changes as staging
build-and-push:
strategy:
matrix:
service:
- your-new-service # ADD HERE

update-ecs-services:
strategy:
matrix:
service:
- your-new-service # ADD HERE

Step 7: Add to Makefile

Update Makefile:

# Service list
SERVICES := api-gateway auth-service tenant-service user-service \
kira-service vera-service data-service notification-service \
your-new-service

# Service will automatically be included in:
# - make build
# - make test
# - make docker-build
# - make docker-up

Step 8: Test Locally

# Build the service
nx build your-new-service

# Run the service
nx serve your-new-service

# Or use Make
make run-service SERVICE=your-new-service

# Test in Docker
docker-compose up your-new-service

# Test entire stack
docker-compose up

Verify:

  • Service starts without errors
  • Health check responds: curl http://localhost:3010/health
  • API Gateway routes to service: curl http://localhost:3000/api/v1/your-resource

Step 9: Write Tests

Create tests in apps/your-new-service/src/:

// app.controller.spec.ts
describe('YourNewController', () => {
let controller: YourNewController;
let service: YourNewService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [YourNewController],
providers: [YourNewService],
}).compile();

controller = module.get<YourNewController>(YourNewController);
service = module.get<YourNewService>(YourNewService);
});

it('should be defined', () => {
expect(controller).toBeDefined();
});

describe('GET /api/v1/your-resource', () => {
it('should return a list of resources', async () => {
const result = await controller.findAll();
expect(Array.isArray(result)).toBe(true);
});
});
});

Run tests:

nx test your-new-service

Step 10: Deploy

Deploy to Staging

git checkout develop
git add .
git commit -m "feat: add your-new-service"
git push origin develop

GitHub Actions will automatically:

  1. Run tests
  2. Build Docker image
  3. Push to ECR
  4. Update Terraform (creates new ECS service)
  5. Deploy to staging
  6. Run health checks

Deploy to Production

git checkout main
git merge develop
git tag -a v1.1.0 -m "Add your-new-service"
git push origin v1.1.0

Workflow will require manual approval before deploying.


Service Checklist

Before deploying, verify:

  • Service cloned from template
  • project.json updated with correct name
  • Port chosen and configured
  • main.ts updated
  • Health endpoint works
  • .env.example created
  • docker-compose.yml updated
  • API Gateway routing configured
  • Terraform ECR module updated
  • Terraform ECS module updated
  • GitHub Actions workflows updated
  • Makefile automatically includes service
  • Tests written and passing
  • README.md documentation complete
  • Tested locally with docker-compose up
  • Tested API Gateway routing
  • Deployed to staging successfully

Service Template Structure

Your service should follow this structure:

apps/your-new-service/
├── src/
│ ├── main.ts # Application entry point
│ ├── app/
│ │ ├── app.module.ts # Root module
│ │ ├── app.controller.ts # Root controller
│ │ └── app.service.ts # Root service
│ ├── health/
│ │ ├── health.module.ts # Health check module
│ │ ├── health.controller.ts # Health endpoint
│ │ └── health.service.ts # Health logic
│ ├── config/ # Configuration
│ ├── entities/ # Prisma entities (if needed)
│ └── assets/
├── .env.example # Environment variables template
├── Dockerfile # Service Dockerfile (optional, uses template)
├── project.json # Nx project configuration
├── tsconfig.json # TypeScript config
├── jest.config.ts # Test configuration
└── README.md # Service documentation

Common Service Patterns

Pattern 1: Service with Database Access

// app.module.ts
import { Module } from '@nestjs/common';
import { DatabaseService } from '@helix/shared/database';

@Module({
imports: [],
providers: [
DatabaseService,
AppService,
],
controllers: [AppController],
})
export class AppModule {}

// app.service.ts
import { Injectable } from '@nestjs/common';
import { DatabaseService } from '@helix/shared/database';

@Injectable()
export class AppService {
constructor(private readonly db: DatabaseService) {}

async findAll() {
return this.db.tenant.yourModel.findMany();
}
}

Pattern 2: Service with External API

// app.module.ts
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';

@Module({
imports: [HttpModule],
providers: [AppService, ExternalApiService],
controllers: [AppController],
})
export class AppModule {}

// external-api.service.ts
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';

@Injectable()
export class ExternalApiService {
constructor(private readonly http: HttpService) {}

async callApi(): Promise<any> {
const { data } = await firstValueFrom(
this.http.get('https://api.example.com/endpoint', {
headers: {
'Authorization': `Bearer ${process.env.YOUR_API_KEY}`,
},
})
);
return data;
}
}

Pattern 3: Service with Redis Caching

// app.module.ts
import { Module } from '@nestjs/common';
import { CacheModule } from '@nestjs/cache-manager';
import { redisStore } from 'cache-manager-redis-store';

@Module({
imports: [
CacheModule.registerAsync({
useFactory: async () => ({
store: await redisStore({
host: process.env.REDIS_HOST,
port: parseInt(process.env.REDIS_PORT || '6379'),
ttl: 300, // 5 minutes default
}),
}),
}),
],
providers: [AppService],
controllers: [AppController],
})
export class AppModule {}

Troubleshooting

Issue: Service not accessible via API Gateway

Check:

  1. Service is running: docker-compose ps
  2. Health endpoint works: curl http://localhost:3010/health
  3. API Gateway routing configured
  4. Network connectivity: docker-compose exec api-gateway ping your-new-service

Issue: Service failing health checks

Debug:

# Check service logs
docker-compose logs -f your-new-service

# Access service directly
curl http://localhost:3010/health

# Check if service is listening
docker-compose exec your-new-service netstat -tlnp

Issue: Terraform plan shows service but won't deploy

Fix:

  1. Ensure service is in ECR module's locals.services list
  2. Verify service is in ECS module's locals.services map
  3. Check GitHub Actions built and pushed image to ECR
  4. Verify image tag matches what Terraform expects

Best Practices

DO ✅

  • Use shared libraries - Import from @helix/shared/*
  • Follow existing patterns - Look at similar services
  • Keep services focused - Single responsibility principle
  • Write comprehensive tests - Unit + integration
  • Document endpoints - Update README with API docs
  • Handle errors gracefully - Use exception filters
  • Add health checks - Include dependencies (DB, Redis)
  • Use TypeScript strict mode - Explicit types everywhere
  • Follow naming conventions - kebab-case for service names

DON'T ❌

  • Don't duplicate code - Use shared libraries
  • Don't skip tests - Minimum 80% coverage
  • Don't hardcode values - Use environment variables
  • Don't expose internal errors - Use filters
  • Don't skip documentation - Update README
  • Don't use any types - TypeScript strict mode
  • Don't forget health checks - Required for deployment
  • Don't commit secrets - Use .env (gitignored)

Port Assignment Reference

PortServiceStatus
3000API Gateway✅ In use
3001Auth Service✅ In use
3002Tenant Service✅ In use
3003User Service🔄 Reserved
3004KIRA Service✅ In use
3005VERA Service✅ In use
3006Data Service🔄 Reserved
3007Notification Service🔄 Reserved
3008Admin Service✅ In use
3009File Service✅ In use
3010+Available⭕ Use next

Example: Complete New Service

See apps/template-service/ for a complete example of a properly configured service with:

  • Health checks
  • Proper module structure
  • Environment configuration
  • Docker support
  • Tests

Clone this as your starting point and customize for your needs.


Need Help?

  • Template service: Check apps/template-service/
  • Similar service: Look at apps/auth-service/ or apps/kira-service/
  • Shared code: See docs/shared-libraries/
  • Deployment: See docs/deployment/deployment-guide.md