Files
labFusion/services/api-docs/CLEAN_CODE.md

12 KiB

API Documentation Service Clean Code Implementation

This document outlines the clean code principles and best practices implemented in the LabFusion API Documentation service (Node.js Express).

🏗️ Architecture Overview

The API Documentation service follows a modular architecture with clear separation of concerns:

api-docs/
├── server.js          # Express application entry point
├── package.json       # Dependencies and scripts
├── Dockerfile         # Production container
├── Dockerfile.dev     # Development container
└── README.md         # Service documentation

🧹 Clean Code Principles Applied

1. Single Responsibility Principle (SRP)

Service Purpose

  • Primary: Aggregates OpenAPI specifications from all services
  • Secondary: Provides unified Swagger UI interface
  • Tertiary: Monitors service health and availability

Function Responsibilities

  • fetchServiceSpec(): Only fetches OpenAPI spec from a single service
  • aggregateSpecs(): Only combines multiple specs into one
  • setupSwaggerUI(): Only configures Swagger UI
  • checkServiceHealth(): Only monitors service health

2. Open/Closed Principle (OCP)

Extensible Service Configuration

// Easy to add new services without modifying existing code
const services = [
  { name: 'api-gateway', url: 'http://api-gateway:8080', specPath: '/v3/api-docs' },
  { name: 'service-adapters', url: 'http://service-adapters:8000', specPath: '/openapi.json' },
  { name: 'api-docs', url: 'http://api-docs:8083', specPath: '/openapi.json' }
  // New services can be added here
];

Configurable Endpoints

// Service URLs can be configured via environment variables
const API_GATEWAY_URL = process.env.API_GATEWAY_URL || 'http://api-gateway:8080';
const SERVICE_ADAPTERS_URL = process.env.SERVICE_ADAPTERS_URL || 'http://service-adapters:8000';

3. Dependency Inversion Principle (DIP)

Abstraction-Based Design

// Depends on abstractions, not concrete implementations
const fetchServiceSpec = async (service) => {
  try {
    const response = await axios.get(`${service.url}${service.specPath}`, {
      timeout: 5000,
      headers: { 'Accept': 'application/json' }
    });
    return { success: true, data: response.data };
  } catch (error) {
    return { success: false, error: error.message };
  }
};

Interface-Based Service Communication

  • Uses HTTP client abstraction (axios)
  • Service health checking through standard endpoints
  • OpenAPI specification standard

4. Interface Segregation Principle (ISP)

Focused API Endpoints

  • /health: Only health checking
  • /services: Only service status
  • /openapi.json: Only OpenAPI specification
  • /: Only Swagger UI interface

📝 Code Quality Improvements

1. Naming Conventions

Clear, Descriptive Names

// Good: Clear purpose
const fetchServiceSpec = async (service) => { /* ... */ };
const aggregateSpecs = (specs) => { /* ... */ };
const checkServiceHealth = async (service) => { /* ... */ };

// Good: Descriptive variable names
const serviceHealthStatus = await checkServiceHealth(service);
const aggregatedSpec = aggregateSpecs(validSpecs);

Consistent Naming

  • Functions: camelCase (e.g., fetchServiceSpec)
  • Variables: camelCase (e.g., serviceHealthStatus)
  • Constants: UPPER_SNAKE_CASE (e.g., API_GATEWAY_URL)
  • Objects: camelCase (e.g., serviceConfig)

2. Function Design

Small, Focused Functions

const fetchServiceSpec = async (service) => {
  try {
    const response = await axios.get(`${service.url}${service.specPath}`, {
      timeout: 5000,
      headers: { 'Accept': 'application/json' }
    });
    return { success: true, data: response.data };
  } catch (error) {
    console.error(`Failed to fetch spec from ${service.name}:`, error.message);
    return { success: false, error: error.message };
  }
};

Single Level of Abstraction

  • Each function handles one concern
  • Error handling separated from business logic
  • Clear return values

3. Error Handling

Consistent Error Responses

const handleError = (error, res) => {
  console.error('Error:', error);
  res.status(500).json({
    error: 'Internal server error',
    message: error.message,
    timestamp: new Date().toISOString()
  });
};

Graceful Degradation

// If some services are unavailable, still serve available specs
const validSpecs = specs.filter(spec => spec.success);
if (validSpecs.length === 0) {
  return res.status(503).json({
    error: 'No services available',
    message: 'All services are currently unavailable'
  });
}

4. Configuration Management

Environment-Based Configuration

const config = {
  port: process.env.PORT || 8083,
  apiGatewayUrl: process.env.API_GATEWAY_URL || 'http://api-gateway:8080',
  serviceAdaptersUrl: process.env.SERVICE_ADAPTERS_URL || 'http://service-adapters:8000',
  apiDocsUrl: process.env.API_DOCS_URL || 'http://api-docs:8083'
};

Centralized Configuration

  • All configuration in one place
  • Environment variable support
  • Default values for development

🔧 Express.js Best Practices

1. Middleware Usage

Appropriate Middleware

app.use(cors());
app.use(express.json());
app.use(express.static('public'));

// Error handling middleware
app.use((error, req, res, next) => {
  handleError(error, res);
});

CORS Configuration

app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
  credentials: true
}));

2. Route Organization

Clear Route Structure

// Health check endpoint
app.get('/health', (req, res) => {
  res.json({
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime()
  });
});

// Service status endpoint
app.get('/services', async (req, res) => {
  try {
    const serviceStatuses = await Promise.allSettled(
      services.map(service => checkServiceHealth(service))
    );
    // Process and return statuses
  } catch (error) {
    handleError(error, res);
  }
});

RESTful Endpoints

  • GET /health: Health check
  • GET /services: Service status
  • GET /openapi.json: OpenAPI specification
  • GET /: Swagger UI

3. Async/Await Usage

Proper Async Handling

app.get('/openapi.json', async (req, res) => {
  try {
    const specs = await Promise.allSettled(
      services.map(service => fetchServiceSpec(service))
    );
    
    const validSpecs = specs
      .filter(result => result.status === 'fulfilled' && result.value.success)
      .map(result => result.value.data);
    
    if (validSpecs.length === 0) {
      return res.status(503).json({
        error: 'No services available',
        message: 'All services are currently unavailable'
      });
    }
    
    const aggregatedSpec = aggregateSpecs(validSpecs);
    res.json(aggregatedSpec);
  } catch (error) {
    handleError(error, res);
  }
});

📊 Data Processing Best Practices

1. OpenAPI Specification Aggregation

Clean Spec Processing

const aggregateSpecs = (specs) => {
  const aggregated = {
    openapi: '3.0.0',
    info: {
      title: 'LabFusion API',
      description: 'Unified API documentation for all LabFusion services',
      version: '1.0.0'
    },
    servers: [],
    paths: {},
    components: {
      schemas: {},
      securitySchemes: {}
    }
  };
  
  specs.forEach(spec => {
    // Merge paths with service prefix
    Object.entries(spec.paths || {}).forEach(([path, methods]) => {
      const prefixedPath = `/${spec.info?.title?.toLowerCase().replace(/\s+/g, '-')}${path}`;
      aggregated.paths[prefixedPath] = methods;
    });
    
    // Merge components
    if (spec.components?.schemas) {
      Object.assign(aggregated.components.schemas, spec.components.schemas);
    }
  });
  
  return aggregated;
};

2. Service Health Monitoring

Robust Health Checking

const checkServiceHealth = async (service) => {
  try {
    const response = await axios.get(`${service.url}/health`, {
      timeout: 5000,
      headers: { 'Accept': 'application/json' }
    });
    
    return {
      name: service.name,
      status: 'healthy',
      responseTime: response.headers['x-response-time'] || 'unknown',
      lastCheck: new Date().toISOString()
    };
  } catch (error) {
    return {
      name: service.name,
      status: 'unhealthy',
      error: error.message,
      lastCheck: new Date().toISOString()
    };
  }
};

🚀 Performance Optimizations

1. Caching Strategy

// Simple in-memory caching for OpenAPI specs
let cachedSpec = null;
let lastFetch = 0;
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes

app.get('/openapi.json', async (req, res) => {
  const now = Date.now();
  
  if (cachedSpec && (now - lastFetch) < CACHE_DURATION) {
    return res.json(cachedSpec);
  }
  
  // Fetch fresh spec
  const spec = await fetchAndAggregateSpecs();
  cachedSpec = spec;
  lastFetch = now;
  
  res.json(spec);
});

2. Error Handling

// Global error handler
process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
});

process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception:', error);
  process.exit(1);
});

3. Resource Management

// Graceful shutdown
process.on('SIGTERM', () => {
  console.log('SIGTERM received, shutting down gracefully');
  server.close(() => {
    console.log('Process terminated');
  });
});

🧪 Testing Strategy

1. Unit Testing

// tests/server.test.js
const request = require('supertest');
const app = require('../server');

describe('API Documentation Service', () => {
  test('GET /health should return healthy status', async () => {
    const response = await request(app).get('/health');
    expect(response.status).toBe(200);
    expect(response.body.status).toBe('healthy');
  });
  
  test('GET /services should return service statuses', async () => {
    const response = await request(app).get('/services');
    expect(response.status).toBe(200);
    expect(Array.isArray(response.body)).toBe(true);
  });
});

2. Integration Testing

test('GET /openapi.json should return aggregated spec', async () => {
  const response = await request(app).get('/openapi.json');
  expect(response.status).toBe(200);
  expect(response.body.openapi).toBe('3.0.0');
  expect(response.body.info.title).toBe('LabFusion API');
});

📋 Code Review Checklist

Function Design

  • Single responsibility per function
  • Clear function names
  • Proper error handling
  • Consistent return values
  • Async/await usage

Error Handling

  • Try-catch blocks where needed
  • Proper HTTP status codes
  • User-friendly error messages
  • Logging for debugging

Configuration

  • Environment variable support
  • Default values
  • Centralized configuration
  • Service-specific settings

Performance

  • Caching where appropriate
  • Timeout handling
  • Resource cleanup
  • Memory management

🎯 Benefits Achieved

1. Maintainability

  • Clear separation of concerns
  • Easy to modify and extend
  • Consistent patterns throughout
  • Self-documenting code

2. Testability

  • Isolated functions
  • Mockable dependencies
  • Clear interfaces
  • Testable business logic

3. Performance

  • Caching for frequently accessed data
  • Efficient error handling
  • Resource management
  • Graceful degradation

4. Scalability

  • Modular architecture
  • Easy to add new services
  • Horizontal scaling support
  • Configuration-driven services

🔮 Future Improvements

Potential Enhancements

  1. Redis Caching: Distributed caching for multiple instances
  2. Metrics: Prometheus metrics integration
  3. Health Checks: More comprehensive health monitoring
  4. Rate Limiting: API rate limiting
  5. Authentication: Service authentication

Monitoring & Observability

  1. Logging: Structured logging with correlation IDs
  2. Tracing: Distributed tracing
  3. Metrics: Service performance metrics
  4. Alerts: Service availability alerts

This clean code implementation makes the API Documentation service more maintainable, testable, and scalable while following Express.js and Node.js best practices.