TypeScript Monorepo with Microservices - Advanced CLAUDE.md Example
Monorepo Architecture Overview
This is an enterprise-grade TypeScript monorepo using:
- Build System: Turborepo with pnpm workspaces
- Services: Multiple microservices (Auth, Users, Orders, Notifications)
- Shared Packages: Common types, utilities, configurations
- Infrastructure: Docker, Kubernetes, GitHub Actions
- Observability: OpenTelemetry, Prometheus, Grafana
- Message Queue: RabbitMQ with TypeScript types
- API Gateway: Kong or custom Node.js gateway
Monorepo Structure
├── apps/
│ ├── api-gateway/ # Kong or Node.js API Gateway
│ ├── auth-service/ # JWT auth microservice
│ ├── user-service/ # User management
│ ├── order-service/ # Order processing
│ ├── notification-service/ # Email/SMS/Push notifications
│ ├── web-app/ # Next.js frontend
│ └── admin-dashboard/ # Admin React app
├── packages/
│ ├── shared-types/ # Shared TypeScript types
│ ├── database/ # Prisma schemas and clients
│ ├── message-queue/ # RabbitMQ abstractions
│ ├── logger/ # Structured logging
│ ├── auth/ # Auth utilities
│ ├── validation/ # Shared Zod schemas
│ ├── config/ # Shared configurations
│ ├── testing/ # Test utilities
│ └── ui-components/ # Shared React components
├── infrastructure/
│ ├── docker/
│ ├── kubernetes/
│ └── terraform/
├── tools/
│ ├── scripts/
│ └── generators/
├── turbo.json
├── pnpm-workspace.yaml
└── package.json
Workspace Configuration
pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
- 'tools/*'turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": [],
"cache": false
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
},
"deploy": {
"dependsOn": ["build", "test"],
"outputs": []
}
}
}Shared Types Package
packages/shared-types/src/index.ts
// Event-driven architecture types
export interface DomainEvent<T = unknown> {
id: string;
type: string;
aggregateId: string;
aggregateType: string;
payload: T;
metadata: EventMetadata;
timestamp: Date;
}
export interface EventMetadata {
userId?: string;
correlationId: string;
causationId?: string;
traceId?: string;
spanId?: string;
}
// Domain types
export interface User {
id: string;
email: string;
profile: UserProfile;
preferences: UserPreferences;
createdAt: Date;
updatedAt: Date;
}
export interface Order {
id: string;
userId: string;
items: OrderItem[];
status: OrderStatus;
total: Money;
createdAt: Date;
updatedAt: Date;
}
export interface Money {
amount: number;
currency: Currency;
}
export enum Currency {
USD = 'USD',
EUR = 'EUR',
GBP = 'GBP',
}
export enum OrderStatus {
PENDING = 'PENDING',
PROCESSING = 'PROCESSING',
SHIPPED = 'SHIPPED',
DELIVERED = 'DELIVERED',
CANCELLED = 'CANCELLED',
}
// API Gateway types
export interface GatewayRequest<T = unknown> {
path: string;
method: HttpMethod;
headers: Record<string, string>;
body?: T;
query?: Record<string, string>;
user?: AuthenticatedUser;
}
export interface GatewayResponse<T = unknown> {
statusCode: number;
headers?: Record<string, string>;
body: T;
}
// Service communication types
export interface ServiceRequest<T = unknown> {
id: string;
service: string;
method: string;
payload: T;
metadata: RequestMetadata;
}
export interface RequestMetadata {
correlationId: string;
userId?: string;
permissions?: string[];
timeout?: number;
}Message Queue Abstraction
packages/message-queue/src/index.ts
import amqp from 'amqplib';
import { DomainEvent, ServiceRequest } from '@company/shared-types';
import { Logger } from '@company/logger';
export interface MessageQueueConfig {
url: string;
exchanges: ExchangeConfig[];
queues: QueueConfig[];
bindings: BindingConfig[];
}
export interface ExchangeConfig {
name: string;
type: 'direct' | 'topic' | 'fanout' | 'headers';
durable?: boolean;
}
export interface QueueConfig {
name: string;
durable?: boolean;
exclusive?: boolean;
autoDelete?: boolean;
arguments?: Record<string, any>;
}
export interface BindingConfig {
queue: string;
exchange: string;
routingKey: string;
}
export class MessageQueue {
private connection: amqp.Connection | null = null;
private channel: amqp.Channel | null = null;
private logger: Logger;
constructor(
private config: MessageQueueConfig,
logger: Logger
) {
this.logger = logger.child({ component: 'MessageQueue' });
}
async connect(): Promise<void> {
try {
this.connection = await amqp.connect(this.config.url);
this.channel = await this.connection.createChannel();
// Set up exchanges, queues, and bindings
await this.setupTopology();
// Handle connection events
this.connection.on('error', this.handleConnectionError.bind(this));
this.connection.on('close', this.handleConnectionClose.bind(this));
this.logger.info('Connected to RabbitMQ');
} catch (error) {
this.logger.error('Failed to connect to RabbitMQ', { error });
throw error;
}
}
async publishEvent<T>(
exchange: string,
routingKey: string,
event: DomainEvent<T>
): Promise<void> {
if (!this.channel) {
throw new Error('Channel not initialized');
}
const message = Buffer.from(JSON.stringify(event));
this.channel.publish(
exchange,
routingKey,
message,
{
persistent: true,
contentType: 'application/json',
headers: {
'x-correlation-id': event.metadata.correlationId,
'x-trace-id': event.metadata.traceId,
},
}
);
this.logger.debug('Published event', {
exchange,
routingKey,
eventType: event.type,
correlationId: event.metadata.correlationId,
});
}
async subscribe<T>(
queue: string,
handler: (event: DomainEvent<T>) => Promise<void>
): Promise<void> {
if (!this.channel) {
throw new Error('Channel not initialized');
}
await this.channel.consume(queue, async (msg) => {
if (!msg) return;
const correlationId = msg.properties.headers['x-correlation-id'];
const startTime = Date.now();
try {
const event = JSON.parse(msg.content.toString()) as DomainEvent<T>;
await handler(event);
this.channel!.ack(msg);
this.logger.debug('Processed event', {
queue,
eventType: event.type,
correlationId,
duration: Date.now() - startTime,
});
} catch (error) {
this.logger.error('Failed to process event', {
queue,
error,
correlationId,
});
// Requeue on failure with exponential backoff
const retryCount = (msg.properties.headers['x-retry-count'] || 0) + 1;
const maxRetries = 3;
if (retryCount <= maxRetries) {
const delay = Math.pow(2, retryCount) * 1000;
setTimeout(() => {
this.channel!.sendToQueue(
queue,
msg.content,
{
...msg.properties,
headers: {
...msg.properties.headers,
'x-retry-count': retryCount,
},
}
);
}, delay);
} else {
// Send to dead letter queue
this.channel!.reject(msg, false);
}
}
});
}
private async setupTopology(): Promise<void> {
if (!this.channel) return;
// Create exchanges
for (const exchange of this.config.exchanges) {
await this.channel.assertExchange(
exchange.name,
exchange.type,
{ durable: exchange.durable ?? true }
);
}
// Create queues
for (const queue of this.config.queues) {
await this.channel.assertQueue(queue.name, {
durable: queue.durable ?? true,
exclusive: queue.exclusive ?? false,
autoDelete: queue.autoDelete ?? false,
arguments: queue.arguments,
});
}
// Create bindings
for (const binding of this.config.bindings) {
await this.channel.bindQueue(
binding.queue,
binding.exchange,
binding.routingKey
);
}
}
private handleConnectionError(error: Error): void {
this.logger.error('RabbitMQ connection error', { error });
// Implement reconnection logic
}
private handleConnectionClose(): void {
this.logger.warn('RabbitMQ connection closed');
// Implement reconnection logic
}
}
// Typed event emitter for internal events
export class TypedEventEmitter<TEvents extends Record<string, any>> {
private emitter = new EventEmitter();
on<K extends keyof TEvents>(
event: K,
listener: (payload: TEvents[K]) => void
): void {
this.emitter.on(event as string, listener);
}
emit<K extends keyof TEvents>(event: K, payload: TEvents[K]): void {
this.emitter.emit(event as string, payload);
}
off<K extends keyof TEvents>(
event: K,
listener: (payload: TEvents[K]) => void
): void {
this.emitter.off(event as string, listener);
}
}Microservice Base Class
packages/microservice-base/src/index.ts
import { Logger } from '@company/logger';
import { MessageQueue } from '@company/message-queue';
import { HealthCheck, ServiceHealth } from '@company/shared-types';
import { createServer, Server } from 'http';
import express, { Application } from 'express';
import { Registry, collectDefaultMetrics } from 'prom-client';
import { trace, context, SpanStatusCode } from '@opentelemetry/api';
export interface MicroserviceConfig {
name: string;
version: string;
port: number;
messageQueue: MessageQueueConfig;
database?: DatabaseConfig;
cache?: CacheConfig;
}
export abstract class Microservice {
protected app: Application;
protected server: Server | null = null;
protected logger: Logger;
protected messageQueue: MessageQueue;
protected registry: Registry;
protected tracer = trace.getTracer('microservice');
constructor(protected config: MicroserviceConfig) {
this.app = express();
this.logger = new Logger({ service: config.name });
this.messageQueue = new MessageQueue(config.messageQueue, this.logger);
this.registry = new Registry();
this.setupMiddleware();
this.setupMetrics();
this.setupHealthChecks();
}
async start(): Promise<void> {
const span = this.tracer.startSpan('service.start');
try {
// Connect to message queue
await this.messageQueue.connect();
// Initialize service-specific resources
await this.initialize();
// Set up routes
await this.setupRoutes();
// Set up event handlers
await this.setupEventHandlers();
// Start HTTP server
this.server = createServer(this.app);
await new Promise<void>((resolve) => {
this.server!.listen(this.config.port, () => {
this.logger.info(`${this.config.name} started`, {
port: this.config.port,
version: this.config.version,
});
resolve();
});
});
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
span.recordException(error as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
}
async stop(): Promise<void> {
this.logger.info(`Stopping ${this.config.name}`);
// Graceful shutdown
if (this.server) {
await new Promise<void>((resolve) => {
this.server!.close(() => resolve());
});
}
await this.cleanup();
await this.messageQueue.disconnect();
}
protected abstract initialize(): Promise<void>;
protected abstract setupRoutes(): Promise<void>;
protected abstract setupEventHandlers(): Promise<void>;
protected abstract cleanup(): Promise<void>;
private setupMiddleware(): void {
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
// Request tracing
this.app.use((req, res, next) => {
const span = this.tracer.startSpan(`${req.method} ${req.path}`);
context.with(trace.setSpan(context.active(), span), () => {
span.setAttributes({
'http.method': req.method,
'http.url': req.url,
'http.target': req.path,
'http.host': req.hostname,
'http.scheme': req.protocol,
'http.user_agent': req.get('user-agent') || '',
});
res.on('finish', () => {
span.setAttributes({
'http.status_code': res.statusCode,
});
span.end();
});
next();
});
});
// Error handling
this.app.use((err: Error, req: any, res: any, next: any) => {
const span = trace.getActiveSpan();
if (span) {
span.recordException(err);
span.setStatus({ code: SpanStatusCode.ERROR });
}
this.logger.error('Unhandled error', {
error: err,
path: req.path,
method: req.method,
});
res.status(500).json({
error: 'Internal server error',
message: process.env.NODE_ENV === 'development' ? err.message : undefined,
});
});
}
private setupMetrics(): void {
collectDefaultMetrics({ register: this.registry });
this.app.get('/metrics', (req, res) => {
res.set('Content-Type', this.registry.contentType);
res.end(this.registry.metrics());
});
}
private setupHealthChecks(): void {
const checks: Record<string, () => Promise<HealthCheck>> = {
messageQueue: async () => ({
status: this.messageQueue.isConnected() ? 'healthy' : 'unhealthy',
message: this.messageQueue.isConnected() ? 'Connected' : 'Disconnected',
}),
};
this.app.get('/health', async (req, res) => {
const results: Record<string, HealthCheck> = {};
let overallStatus = 'healthy';
for (const [name, check] of Object.entries(checks)) {
try {
results[name] = await check();
if (results[name].status === 'unhealthy') {
overallStatus = 'unhealthy';
}
} catch (error) {
results[name] = {
status: 'unhealthy',
message: error.message,
};
overallStatus = 'unhealthy';
}
}
const health: ServiceHealth = {
service: this.config.name,
version: this.config.version,
status: overallStatus as 'healthy' | 'unhealthy',
checks: results,
timestamp: new Date(),
};
res.status(overallStatus === 'healthy' ? 200 : 503).json(health);
});
this.app.get('/ready', (req, res) => {
// Kubernetes readiness probe
res.sendStatus(200);
});
this.app.get('/live', (req, res) => {
// Kubernetes liveness probe
res.sendStatus(200);
});
}
}Auth Service Implementation
apps/auth-service/src/index.ts
import { Microservice } from '@company/microservice-base';
import { User, DomainEvent } from '@company/shared-types';
import { PrismaClient } from '@company/database';
import { z } from 'zod';
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { Redis } from 'ioredis';
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
const registerSchema = loginSchema.extend({
name: z.string().min(2),
});
export class AuthService extends Microservice {
private prisma!: PrismaClient;
private redis!: Redis;
async initialize(): Promise<void> {
this.prisma = new PrismaClient();
this.redis = new Redis(process.env.REDIS_URL!);
await this.prisma.$connect();
this.logger.info('Connected to database');
}
async setupRoutes(): Promise<void> {
// Register endpoint
this.app.post('/auth/register', async (req, res) => {
const span = this.tracer.startSpan('auth.register');
try {
const data = registerSchema.parse(req.body);
// Check if user exists
const existing = await this.prisma.user.findUnique({
where: { email: data.email },
});
if (existing) {
return res.status(409).json({
error: 'User already exists',
});
}
// Hash password
const hashedPassword = await bcrypt.hash(data.password, 10);
// Create user
const user = await this.prisma.user.create({
data: {
email: data.email,
name: data.name,
password: hashedPassword,
},
select: {
id: true,
email: true,
name: true,
},
});
// Publish event
await this.messageQueue.publishEvent('user.exchange', 'user.created', {
id: crypto.randomUUID(),
type: 'user.created',
aggregateId: user.id,
aggregateType: 'user',
payload: user,
metadata: {
correlationId: req.headers['x-correlation-id'] as string || crypto.randomUUID(),
userId: user.id,
},
timestamp: new Date(),
});
// Generate tokens
const { accessToken, refreshToken } = this.generateTokens(user);
res.status(201).json({
user,
accessToken,
refreshToken,
});
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
span.recordException(error as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
if (error instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation failed',
details: error.errors,
});
}
throw error;
} finally {
span.end();
}
});
// Login endpoint
this.app.post('/auth/login', async (req, res) => {
const span = this.tracer.startSpan('auth.login');
try {
const data = loginSchema.parse(req.body);
// Find user
const user = await this.prisma.user.findUnique({
where: { email: data.email },
});
if (!user || !(await bcrypt.compare(data.password, user.password))) {
return res.status(401).json({
error: 'Invalid credentials',
});
}
// Generate tokens
const { accessToken, refreshToken } = this.generateTokens(user);
// Store refresh token in Redis
await this.redis.setex(
`refresh_token:${user.id}`,
30 * 24 * 60 * 60, // 30 days
refreshToken
);
res.json({
user: {
id: user.id,
email: user.email,
name: user.name,
},
accessToken,
refreshToken,
});
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
span.recordException(error as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
if (error instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation failed',
details: error.errors,
});
}
throw error;
} finally {
span.end();
}
});
// Token refresh endpoint
this.app.post('/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({
error: 'Refresh token required',
});
}
try {
const payload = jwt.verify(
refreshToken,
process.env.JWT_REFRESH_SECRET!
) as jwt.JwtPayload;
// Verify token in Redis
const storedToken = await this.redis.get(`refresh_token:${payload.sub}`);
if (storedToken !== refreshToken) {
return res.status(401).json({
error: 'Invalid refresh token',
});
}
// Get user
const user = await this.prisma.user.findUnique({
where: { id: payload.sub },
});
if (!user) {
return res.status(401).json({
error: 'User not found',
});
}
// Generate new tokens
const tokens = this.generateTokens(user);
// Update stored refresh token
await this.redis.setex(
`refresh_token:${user.id}`,
30 * 24 * 60 * 60,
tokens.refreshToken
);
res.json(tokens);
} catch (error) {
return res.status(401).json({
error: 'Invalid refresh token',
});
}
});
// Logout endpoint
this.app.post('/auth/logout', async (req, res) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
error: 'Token required',
});
}
try {
const payload = jwt.verify(
token,
process.env.JWT_SECRET!
) as jwt.JwtPayload;
// Remove refresh token
await this.redis.del(`refresh_token:${payload.sub}`);
// Add access token to blacklist
const ttl = payload.exp! - Math.floor(Date.now() / 1000);
if (ttl > 0) {
await this.redis.setex(`blacklist:${token}`, ttl, '1');
}
res.status(204).send();
} catch (error) {
return res.status(401).json({
error: 'Invalid token',
});
}
});
// Verify endpoint for other services
this.app.get('/auth/verify', async (req, res) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
error: 'Token required',
});
}
try {
// Check blacklist
const blacklisted = await this.redis.get(`blacklist:${token}`);
if (blacklisted) {
return res.status(401).json({
error: 'Token revoked',
});
}
const payload = jwt.verify(
token,
process.env.JWT_SECRET!
) as jwt.JwtPayload;
const user = await this.prisma.user.findUnique({
where: { id: payload.sub },
select: {
id: true,
email: true,
name: true,
role: true,
permissions: true,
},
});
if (!user) {
return res.status(401).json({
error: 'User not found',
});
}
res.json({ user });
} catch (error) {
return res.status(401).json({
error: 'Invalid token',
});
}
});
}
async setupEventHandlers(): Promise<void> {
// Listen for user events from other services
await this.messageQueue.subscribe('auth.user.queue', async (event: DomainEvent) => {
switch (event.type) {
case 'user.deleted':
await this.handleUserDeleted(event);
break;
case 'user.suspended':
await this.handleUserSuspended(event);
break;
}
});
}
async cleanup(): Promise<void> {
await this.prisma.$disconnect();
await this.redis.quit();
}
private generateTokens(user: any) {
const accessToken = jwt.sign(
{
sub: user.id,
email: user.email,
role: user.role,
},
process.env.JWT_SECRET!,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ sub: user.id },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: '30d' }
);
return { accessToken, refreshToken };
}
private async handleUserDeleted(event: DomainEvent<{ userId: string }>) {
// Revoke all tokens for deleted user
await this.redis.del(`refresh_token:${event.payload.userId}`);
// Add logic to blacklist all active access tokens
}
private async handleUserSuspended(event: DomainEvent<{ userId: string }>) {
// Similar to deleted, but might want different handling
await this.redis.del(`refresh_token:${event.payload.userId}`);
}
}
// Start the service
const service = new AuthService({
name: 'auth-service',
version: '1.0.0',
port: parseInt(process.env.PORT || '3001'),
messageQueue: {
url: process.env.RABBITMQ_URL!,
exchanges: [
{ name: 'user.exchange', type: 'topic' },
{ name: 'auth.exchange', type: 'topic' },
],
queues: [
{ name: 'auth.user.queue' },
],
bindings: [
{ queue: 'auth.user.queue', exchange: 'user.exchange', routingKey: 'user.*' },
],
},
});
service.start().catch((error) => {
console.error('Failed to start auth service:', error);
process.exit(1);
});
// Graceful shutdown
process.on('SIGTERM', async () => {
await service.stop();
process.exit(0);
});API Gateway Implementation
apps/api-gateway/src/index.ts
import express from 'express';
import httpProxy from 'http-proxy-middleware';
import { Logger } from '@company/logger';
import { RateLimiter } from './rateLimiter';
import { CircuitBreaker } from './circuitBreaker';
import { AuthMiddleware } from './authMiddleware';
interface ServiceConfig {
name: string;
url: string;
prefix: string;
rateLimit?: {
windowMs: number;
maxRequests: number;
};
circuitBreaker?: {
threshold: number;
timeout: number;
resetTimeout: number;
};
auth?: boolean;
}
const services: ServiceConfig[] = [
{
name: 'auth',
url: process.env.AUTH_SERVICE_URL || 'http://auth-service:3001',
prefix: '/auth',
rateLimit: {
windowMs: 60000,
maxRequests: 10,
},
},
{
name: 'users',
url: process.env.USER_SERVICE_URL || 'http://user-service:3002',
prefix: '/users',
auth: true,
circuitBreaker: {
threshold: 5,
timeout: 3000,
resetTimeout: 30000,
},
},
{
name: 'orders',
url: process.env.ORDER_SERVICE_URL || 'http://order-service:3003',
prefix: '/orders',
auth: true,
},
];
class ApiGateway {
private app = express();
private logger = new Logger({ service: 'api-gateway' });
private rateLimiters = new Map<string, RateLimiter>();
private circuitBreakers = new Map<string, CircuitBreaker>();
private authMiddleware = new AuthMiddleware();
constructor() {
this.setupMiddleware();
this.setupRoutes();
}
private setupMiddleware(): void {
// CORS
this.app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', process.env.CORS_ORIGIN || '*');
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
// Request ID
this.app.use((req, res, next) => {
req.headers['x-request-id'] = req.headers['x-request-id'] || crypto.randomUUID();
req.headers['x-correlation-id'] = req.headers['x-correlation-id'] || req.headers['x-request-id'];
next();
});
// Logging
this.app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
this.logger.info('Request processed', {
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration: Date.now() - start,
requestId: req.headers['x-request-id'],
});
});
next();
});
}
private setupRoutes(): void {
for (const service of services) {
// Set up rate limiter
if (service.rateLimit) {
this.rateLimiters.set(
service.name,
new RateLimiter(service.rateLimit)
);
}
// Set up circuit breaker
if (service.circuitBreaker) {
this.circuitBreakers.set(
service.name,
new CircuitBreaker(service.circuitBreaker)
);
}
// Create proxy middleware
const proxy = httpProxy.createProxyMiddleware({
target: service.url,
changeOrigin: true,
pathRewrite: {
[`^${service.prefix}`]: '',
},
onProxyReq: (proxyReq, req) => {
// Forward correlation ID
if (req.headers['x-correlation-id']) {
proxyReq.setHeader('x-correlation-id', req.headers['x-correlation-id']);
}
if (req.headers['x-request-id']) {
proxyReq.setHeader('x-request-id', req.headers['x-request-id']);
}
},
onError: (err, req, res) => {
this.logger.error('Proxy error', {
service: service.name,
error: err.message,
path: req.path,
});
res.status(502).json({
error: 'Service unavailable',
service: service.name,
});
},
});
// Apply middleware chain
const middlewares: express.RequestHandler[] = [];
// Rate limiting
const rateLimiter = this.rateLimiters.get(service.name);
if (rateLimiter) {
middlewares.push((req, res, next) =>
rateLimiter.middleware(req, res, next)
);
}
// Authentication
if (service.auth) {
middlewares.push((req, res, next) =>
this.authMiddleware.verify(req, res, next)
);
}
// Circuit breaker
const circuitBreaker = this.circuitBreakers.get(service.name);
if (circuitBreaker) {
middlewares.push((req, res, next) => {
if (circuitBreaker.isOpen()) {
return res.status(503).json({
error: 'Service temporarily unavailable',
service: service.name,
});
}
const originalSend = res.send;
res.send = function(data) {
if (res.statusCode >= 500) {
circuitBreaker.recordFailure();
} else {
circuitBreaker.recordSuccess();
}
return originalSend.call(this, data);
};
next();
});
}
// Apply route
this.app.use(service.prefix, ...middlewares, proxy);
}
// Health check
this.app.get('/health', (req, res) => {
const health = {
status: 'healthy',
services: {} as Record<string, string>,
};
for (const service of services) {
const breaker = this.circuitBreakers.get(service.name);
health.services[service.name] = breaker?.isOpen() ? 'unhealthy' : 'healthy';
}
const overallHealthy = Object.values(health.services).every(s => s === 'healthy');
res.status(overallHealthy ? 200 : 503).json(health);
});
}
start(port: number): void {
this.app.listen(port, () => {
this.logger.info('API Gateway started', { port });
});
}
}
// Start gateway
const gateway = new ApiGateway();
gateway.start(parseInt(process.env.PORT || '3000'));Testing Strategy
Integration Test Example
// tests/integration/order-flow.test.ts
import { TestContainers } from '@company/testing';
import { OrderService } from '@apps/order-service';
import { MessageQueue } from '@company/message-queue';
describe('Order Processing Flow', () => {
let containers: TestContainers;
let orderService: OrderService;
let messageQueue: MessageQueue;
beforeAll(async () => {
containers = new TestContainers();
// Start required containers
await containers.startPostgres();
await containers.startRedis();
await containers.startRabbitMQ();
// Initialize services
orderService = new OrderService({
// Test config
});
await orderService.start();
});
afterAll(async () => {
await orderService.stop();
await containers.stopAll();
});
it('should process order from creation to completion', async () => {
// Create order
const order = await orderService.createOrder({
userId: 'test-user',
items: [
{ productId: 'prod-1', quantity: 2, price: 29.99 },
],
});
expect(order.status).toBe('PENDING');
// Verify events were published
const events = await messageQueue.getPublishedEvents('order.created');
expect(events).toHaveLength(1);
expect(events[0].aggregateId).toBe(order.id);
// Simulate payment processing
await messageQueue.publishEvent('payment.exchange', 'payment.completed', {
id: crypto.randomUUID(),
type: 'payment.completed',
aggregateId: order.id,
aggregateType: 'order',
payload: {
orderId: order.id,
amount: 59.98,
currency: 'USD',
},
metadata: {
correlationId: crypto.randomUUID(),
},
timestamp: new Date(),
});
// Wait for order to be updated
await new Promise(resolve => setTimeout(resolve, 1000));
// Verify order status
const updatedOrder = await orderService.getOrder(order.id);
expect(updatedOrder.status).toBe('PROCESSING');
});
});Deployment Configuration
Kubernetes Manifests
# infrastructure/kubernetes/auth-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-service
labels:
app: auth-service
spec:
replicas: 3
selector:
matchLabels:
app: auth-service
template:
metadata:
labels:
app: auth-service
spec:
containers:
- name: auth-service
image: company/auth-service:latest
ports:
- containerPort: 3001
env:
- name: NODE_ENV
value: "production"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: database-secrets
key: url
- name: RABBITMQ_URL
valueFrom:
secretKeyRef:
name: rabbitmq-secrets
key: url
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: auth-secrets
key: jwt-secret
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /live
port: 3001
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3001
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: auth-service
spec:
selector:
app: auth-service
ports:
- port: 3001
targetPort: 3001
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: auth-service-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: auth-service
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80Monitoring and Observability
OpenTelemetry Setup
// packages/observability/src/tracing.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
export function initializeTracing(serviceName: string) {
const traceExporter = new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
});
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: serviceName,
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.SERVICE_VERSION || '1.0.0',
}),
traceExporter,
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': {
enabled: false,
},
}),
],
});
sdk.start();
return sdk;
}Workshop Exercises
- Add New Microservice: Create a notification service that handles email/SMS
- Implement Saga Pattern: Build distributed transaction handling
- Add GraphQL Gateway: Create GraphQL layer on top of microservices
- Implement CQRS: Separate read/write models with event sourcing
- Add Service Mesh: Integrate Istio or Linkerd for advanced traffic management
Common Pitfalls in Monorepos
- Circular Dependencies
// ❌ Bad: Circular dependency
// packages/auth/index.ts
import { UserService } from '@company/users';
// packages/users/index.ts
import { AuthService } from '@company/auth';
// ✅ Good: Use events or shared interfaces
// packages/shared-types/auth.ts
export interface AuthProvider {
verifyToken(token: string): Promise<User>;
}
// packages/auth/index.ts
export class AuthService implements AuthProvider {
// Implementation
}- Version Mismatches
// ✅ Good: Use workspace protocol
{
"dependencies": {
"@company/shared-types": "workspace:*",
"@company/logger": "workspace:*"
}
}- Build Order Issues
// turbo.json - Proper dependency graph
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
}
}
}Remember: In a monorepo, consistency is key. Establish patterns early and enforce them through tooling!