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: 80

Monitoring 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

  1. Add New Microservice: Create a notification service that handles email/SMS
  2. Implement Saga Pattern: Build distributed transaction handling
  3. Add GraphQL Gateway: Create GraphQL layer on top of microservices
  4. Implement CQRS: Separate read/write models with event sourcing
  5. Add Service Mesh: Integrate Istio or Linkerd for advanced traffic management

Common Pitfalls in Monorepos

  1. 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
}
  1. Version Mismatches
// ✅ Good: Use workspace protocol
{
  "dependencies": {
    "@company/shared-types": "workspace:*",
    "@company/logger": "workspace:*"
  }
}
  1. 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!