12 KiB
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 serviceaggregateSpecs(): Only combines multiple specs into onesetupSwaggerUI(): Only configures Swagger UIcheckServiceHealth(): 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
- Redis Caching: Distributed caching for multiple instances
- Metrics: Prometheus metrics integration
- Health Checks: More comprehensive health monitoring
- Rate Limiting: API rate limiting
- Authentication: Service authentication
Monitoring & Observability
- Logging: Structured logging with correlation IDs
- Tracing: Distributed tracing
- Metrics: Service performance metrics
- 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.