API Best Practices

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
  1. Secure Credential Storage - Store API credentials and tokens securely. Never expose them in client-side code or source repositories.
  2. HTTPS Only - Always use HTTPS for all API calls to ensure data is encrypted in transit.
  3. Least Privilege - Create dedicated API users with only the permissions necessary for your integration.
  4. Input Validation - Validate all inputs before sending them to the API to prevent injection attacks.
  5. 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.

Secure Credential Storage Example (Node.js)
// 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
  1. Minimize API Calls - Cache data when appropriate to reduce the number of API calls.
  2. Batch Operations - Use group, school, or district-level commands instead of individual light commands when possible.
  3. Handle Rate Limits - Implement backoff strategies if you encounter rate limiting.
  4. Optimize Queries - Only request the data you need by using appropriate filters.
  5. 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.

Efficient Command Dispatch Example
// 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 Example
// 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
  1. Comprehensive Error Handling - Handle all possible error responses from the API.
  2. Graceful Degradation - Design your application to function partially when some API calls fail.
  3. User-Friendly Messages - Translate API error messages into user-friendly notifications.
  4. Retry Strategies - Implement appropriate retry strategies for transient errors.
  5. Connection Errors - Handle network connectivity issues gracefully.
  6. 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.

Retry Strategy Example
/**
 * 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 Translation Example
// 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
  1. Command Confirmation - Always verify that commands were sent successfully by checking response status.
  2. Emergency Procedures - Have clear procedures for emergency situations that require sending commands.
  3. Drill Mode - Consider implementing a "drill mode" that clearly indicates when commands are part of a drill.
  4. Command Throttling - Avoid sending too many commands in quick succession to prevent overloading the system.
  5. Verification - Use the history API to verify that commands were received and processed correctly.
  6. 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.

Command Confirmation Example
/**
 * 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.

Drill Mode Example
/**
 * 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
  1. Separation of Concerns - Organize your code into logical components as demonstrated in the complete system example.
  2. Configuration Management - Externalize configuration such as API endpoints and credentials.
  3. Logging Strategy - Implement comprehensive logging for debugging and auditing purposes.
  4. Testing Strategy - Develop a testing strategy that includes unit tests, integration tests, and end-to-end tests.
  5. Error Monitoring - Implement error monitoring to catch and address issues in production.
  6. 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 Management Example
// 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.

Logging Strategy Example
// 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()
    });
  }
};