475 lines
12 KiB
Markdown
475 lines
12 KiB
Markdown
# 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.
|