API Best Practices
Table of Contents
Overview
When integrating with the RCAMS API, following these best practices will help ensure reliable, secure, and efficient operation of your application. This guide provides recommendations for security, performance, error handling, and architecture to help you build robust integrations.
Note: The RCAMS system is designed for emergency notifications in educational settings. As such, reliability, security, and proper operation are critical considerations in any integration.
Security Considerations
Security is paramount when dealing with emergency notification systems. Follow these best practices to ensure your integration is secure.
Security Best Practices
- Secure Credential Storage - Store API credentials and tokens securely. Never expose them in client-side code or source repositories.
- HTTPS Only - Always use HTTPS for all API calls to ensure data is encrypted in transit.
- Least Privilege - Create dedicated API users with only the permissions necessary for your integration.
- Input Validation - Validate all inputs before sending them to the API to prevent injection attacks.
- Audit Logging - Maintain logs of API calls, especially for sensitive operations like sending emergency commands.
API Credential Security
The API credentials provided by your RCAMS administrator must be stored securely and never exposed to end users.
// Using environment variables for credential storage
require('dotenv').config(); // Load environment variables from .env file
// Access credentials from environment variables
const apiUsername = process.env.RCAMS_API_USERNAME;
const apiPassword = process.env.RCAMS_API_PASSWORD;
// Verify that credentials are available
if (!apiUsername || !apiPassword) {
throw new Error('RCAMS API credentials not found in environment variables');
}
// Use credentials in API requests
function getAuthHeader() {
return 'Basic ' + Buffer.from(`${apiUsername}:${apiPassword}`).toString('base64');
}
For web applications, consider using secure HTTP-only cookies or secure client-side storage with appropriate security measures to store tokens.
Performance Optimization
Optimize your integration for better performance and responsiveness.
Performance Best Practices
- Minimize API Calls - Cache data when appropriate to reduce the number of API calls.
- Batch Operations - Use group, school, or district-level commands instead of individual light commands when possible.
- Handle Rate Limits - Implement backoff strategies if you encounter rate limiting.
- Optimize Queries - Only request the data you need by using appropriate filters.
- Connection Reuse - Reuse HTTP connections when making multiple requests to improve performance.
Batch Commands
When sending commands to multiple lights, use the group, school, or district command endpoints rather than sending individual commands to each light. This reduces the number of API calls and improves performance.
// Inefficient: Sending commands to individual lights
async function sendLockdownToAllLights(client, lights) {
// DON'T DO THIS - Inefficient approach
for (const light of lights) {
await client.commands.sendToLight(light.light_mac, 'MODE:1');
}
}
// Efficient: Using group command
async function sendLockdownToGroup(client, groupId) {
// DO THIS - More efficient approach
await client.commands.sendToGroup(groupId, 'MODE:1');
}
Caching Strategy
Implement caching for relatively static data such as districts, schools, and groups to reduce API calls and improve responsiveness.
// Simple caching mechanism
class CachedDistrictManager {
constructor(client) {
this.client = client;
this.cache = {
districts: null,
lastFetched: 0,
maxAge: 3600000 // 1 hour in milliseconds
};
}
async list() {
const now = Date.now();
// Check if cache is valid
if (this.cache.districts && (now - this.cache.lastFetched) < this.cache.maxAge) {
console.log('Using cached district list');
return this.cache.districts;
}
// Fetch fresh data and update cache
console.log('Fetching fresh district list');
const districts = await this.client.districts.list();
this.cache.districts = districts;
this.cache.lastFetched = now;
return districts;
}
}
Error Handling
Implement robust error handling to ensure your application can recover gracefully from failures.
Error Handling Best Practices
- Comprehensive Error Handling - Handle all possible error responses from the API.
- Graceful Degradation - Design your application to function partially when some API calls fail.
- User-Friendly Messages - Translate API error messages into user-friendly notifications.
- Retry Strategies - Implement appropriate retry strategies for transient errors.
- Connection Errors - Handle network connectivity issues gracefully.
- Timeout Handling - Set appropriate timeouts for API calls and handle timeout errors.
Retry Strategy
Implement a retry strategy for transient errors such as network issues or server overload.
/**
* Makes an API request with retry capability
* @param {Function} requestFn - Function that makes the actual request
* @param {Object} options - Retry options
* @returns {Promise<*>} - Promise resolving to the API response
*/
async function withRetry(requestFn, options = {}) {
const maxRetries = options.maxRetries || 3;
const baseDelay = options.baseDelay || 1000; // 1 second
const maxDelay = options.maxDelay || 10000; // 10 seconds
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await requestFn();
} catch (error) {
lastError = error;
// Check if error is retryable
if (!isRetryableError(error)) {
throw error; // Don't retry non-retryable errors
}
if (attempt < maxRetries - 1) {
// Calculate exponential backoff delay with jitter
const delay = Math.min(
maxDelay,
baseDelay * Math.pow(2, attempt) * (0.5 + Math.random())
);
console.log(`Request failed, retrying in ${Math.round(delay)}ms (Attempt ${attempt + 1}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
// If we get here, all retry attempts failed
throw lastError;
}
// Helper to determine if an error is retryable
function isRetryableError(error) {
// Network errors are retryable
if (error.name === 'NetworkError' || error.message.includes('network')) {
return true;
}
// Server errors (5xx) are retryable
if (error.status && error.status >= 500 && error.status < 600) {
return true;
}
// Rate limiting (429) is retryable
if (error.status === 429) {
return true;
}
// Other errors are not retryable
return false;
}
// Example usage
async function sendCommandWithRetry(client, lightMac, command) {
return withRetry(
() => client.commands.sendToLight(lightMac, command),
{ maxRetries: 3, baseDelay: 1000 }
);
}
Error Translation
Translate API error messages into user-friendly notifications that help users understand and resolve issues.
// Error translator
function translateApiError(error) {
// Standard error messages
const errorMessages = {
'Authorization denied': 'Your API credentials are invalid. Please contact your administrator.',
'missed_params': 'Missing required parameters in the request.',
'Invalid action': 'The requested action is not supported.',
'not_found': 'The requested resource was not found.',
'light_missed_params': 'The light configuration is incomplete.',
'Failed to connect to MQTT broker': 'Unable to connect to the light control system. Please check network connectivity.'
};
// Extract error message
let message = '';
if (typeof error === 'string') {
message = error;
} else if (error.message) {
message = error.message;
} else if (error.status && error.status !== 'OK') {
message = error.message || 'An unknown error occurred';
}
// Look up user-friendly message or use a generic one
return errorMessages[message] || `Error: ${message}`;
}
Light Command Best Practices
Sending commands to lights is the primary function of the RCAMS system and requires special consideration.
Light Command Best Practices
- Command Confirmation - Always verify that commands were sent successfully by checking response status.
- Emergency Procedures - Have clear procedures for emergency situations that require sending commands.
- Drill Mode - Consider implementing a "drill mode" that clearly indicates when commands are part of a drill.
- Command Throttling - Avoid sending too many commands in quick succession to prevent overloading the system.
- Verification - Use the history API to verify that commands were received and processed correctly.
- User Permissions - Strictly control which users can send emergency commands, especially district-wide commands.
Command Confirmation and Feedback
Always check the API response to confirm that commands were sent successfully and provide appropriate feedback to users.
/**
* Sends a command with confirmation and user feedback
*/
async function sendConfirmedCommand(client, commandType, target, targetId, userInterface) {
try {
// Show sending status
userInterface.showStatus('Sending command...');
// Send the command
const response = await client.commands.sendCommand(target, targetId, commandType);
// Check if command was successful
if (response.status === 'OK') {
userInterface.showSuccess('Command sent successfully');
// For group, school, or district commands, show detailed results
if (response.results) {
// Count successes and failures
const successes = response.results.filter(r => r.status === 'OK').length;
const failures = response.results.length - successes;
if (failures > 0) {
userInterface.showWarning(`Command partially successful: ${successes} succeeded, ${failures} failed.`);
} else {
userInterface.showSuccess(`Command sent to all ${response.results.length} lights successfully.`);
}
}
return true;
} else {
userInterface.showError(`Failed to send command: ${response.message}`);
return false;
}
} catch (error) {
userInterface.showError(`Error sending command: ${error.message}`);
return false;
}
}
Drill Mode Implementation
Implement a "drill mode" to clearly indicate when commands are part of a training exercise rather than an actual emergency.
/**
* Emergency Command System with Drill Mode
*/
class EmergencyCommandSystem {
constructor(client) {
this.client = client;
this.isDrillMode = false;
this.drillLogEntries = [];
}
// Enable or disable drill mode
setDrillMode(enabled) {
this.isDrillMode = enabled;
console.log(`Drill mode ${enabled ? 'enabled' : 'disabled'}`);
// Clear drill log when disabling
if (!enabled) {
this.drillLogEntries = [];
}
}
// Log drill command
logDrillCommand(target, targetId, command, timestamp = new Date()) {
this.drillLogEntries.push({
timestamp,
target,
targetId,
command,
});
}
// Get drill log
getDrillLog() {
return this.drillLogEntries;
}
// Send command (real or drill)
async sendCommand(target, targetId, command) {
if (this.isDrillMode) {
// In drill mode, log the command but don't actually send it
console.log(`[DRILL MODE] Would send ${command} to ${target} ${targetId}`);
this.logDrillCommand(target, targetId, command);
// Return a simulated successful response
return {
status: 'OK',
message: 'Drill command logged successfully',
isDrill: true
};
} else {
// In real mode, actually send the command
return await this.client.commands.sendCommand(target, targetId, command);
}
}
// Send lockdown command (real or drill)
async sendLockdown(target, targetId) {
return this.sendCommand(target, targetId, LIGHT_COMMANDS.LOCKDOWN);
}
// Additional command methods...
}
Integration Architecture
Design your integration architecture for maintainability, scalability, and reliability.
Integration Architecture Best Practices
- Separation of Concerns - Organize your code into logical components as demonstrated in the complete system example.
- Configuration Management - Externalize configuration such as API endpoints and credentials.
- Logging Strategy - Implement comprehensive logging for debugging and auditing purposes.
- Testing Strategy - Develop a testing strategy that includes unit tests, integration tests, and end-to-end tests.
- Error Monitoring - Implement error monitoring to catch and address issues in production.
- Documentation - Document your integration thoroughly, including authentication flows, error handling, and common use cases.
Configuration Management
Externalize configuration to make your integration more flexible and easier to maintain.
// Configuration file (config.js)
const config = {
// API configuration
api: {
baseUrl: process.env.RCAMS_API_URL || 'https://rcamsapi.spheronomics.com/api/v1',
username: process.env.RCAMS_API_USERNAME,
password: process.env.RCAMS_API_PASSWORD,
timeout: parseInt(process.env.RCAMS_API_TIMEOUT || '30000', 10), // 30 seconds
},
// Command configuration
commands: {
retryAttempts: 3,
retryDelay: 1000, // 1 second base delay
batchSize: 10, // Number of lights to process in one batch
},
// Logging configuration
logging: {
level: process.env.LOG_LEVEL || 'info',
file: process.env.LOG_FILE || './logs/rcams.log',
console: process.env.LOG_CONSOLE !== 'false',
}
};
// Validate required configuration
function validateConfig() {
const requiredFields = [
'api.username',
'api.password'
];
for (const field of requiredFields) {
const value = field.split('.').reduce((obj, key) => obj && obj[key], config);
if (!value) {
throw new Error(`Missing required configuration: ${field}`);
}
}
return config;
}
export default validateConfig();
Logging Strategy
Implement comprehensive logging for debugging and auditing purposes, especially for critical operations like sending emergency commands.
// Logger module (logger.js)
import winston from 'winston';
import config from './config';
// Create a Winston logger
const logger = winston.createLogger({
level: config.logging.level,
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: { service: 'rcams-client' },
transports: [
// Write logs to file
new winston.transports.File({
filename: config.logging.file,
level: config.logging.level
})
]
});
// Add console transport if enabled
if (config.logging.console) {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
// Add audit logging for emergency commands
const auditLogger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
defaultMeta: { service: 'rcams-audit' },
transports: [
new winston.transports.File({
filename: './logs/rcams-audit.log'
})
]
});
// Export logger functions
export default {
// Standard logging
debug: (message, meta = {}) => logger.debug(message, meta),
info: (message, meta = {}) => logger.info(message, meta),
warn: (message, meta = {}) => logger.warn(message, meta),
error: (message, meta = {}) => logger.error(message, meta),
// Audit logging for emergency commands
auditCommand: (user, command, target, result) => {
auditLogger.info('Command issued', {
user,
command,
target,
result,
timestamp: new Date().toISOString()
});
}
};