Update README and documentation; refactor frontend components for improved structure and resilience

This commit is contained in:
glenn schrooyen
2025-09-11 23:46:29 +02:00
parent 63b4bb487d
commit b9206de1a0
49 changed files with 27058 additions and 581 deletions

View File

@@ -0,0 +1,474 @@
# 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**
```javascript
// 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**
```javascript
// 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**
```javascript
// 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**
```javascript
// 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**
```javascript
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**
```javascript
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**
```javascript
// 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**
```javascript
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**
```javascript
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**
```javascript
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true
}));
```
### **2. Route Organization**
#### **Clear Route Structure**
```javascript
// 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**
```javascript
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**
```javascript
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**
```javascript
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**
```javascript
// 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**
```javascript
// 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**
```javascript
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
server.close(() => {
console.log('Process terminated');
});
});
```
## 🧪 **Testing Strategy**
### **1. Unit Testing**
```javascript
// 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**
```javascript
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.