Overview

This guide covers best practices for building robust, efficient, and secure integrations with the GetBill API. Following these guidelines will help ensure your application performs well and provides a great user experience.

Authentication & Security

Secure Token Storage

Store access tokens securely and never expose them in client-side code or logs.

Token Refresh

Implement automatic token refresh to handle expiration gracefully.

Minimal Scopes

Request only the minimum scopes required for your application’s functionality.

HTTPS Only

Always use HTTPS for all API communications in production.

Token Management Example

class TokenManager {
  constructor(clientId, clientSecret) {
    this.clientId = clientId;
    this.clientSecret = clientSecret;
    this.accessToken = null;
    this.refreshToken = null;
    this.expiresAt = null;
  }

  async getValidToken() {
    // Check if current token is still valid
    if (this.accessToken && this.expiresAt > Date.now() + 60000) {
      return this.accessToken;
    }

    // Try to refresh if we have a refresh token
    if (this.refreshToken) {
      try {
        await this.refreshAccessToken();
        return this.accessToken;
      } catch (error) {
        console.log('Token refresh failed, getting new token');
      }
    }

    // Get a new token
    await this.getNewToken();
    return this.accessToken;
  }

  async refreshAccessToken() {
    const response = await fetch('https://getbill.io/oauth/token', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
      },
      body: new URLSearchParams({
        grant_type: 'refresh_token',
        refresh_token: this.refreshToken,
        client_id: this.clientId,
        client_secret: this.clientSecret
      })
    });

    if (!response.ok) {
      throw new Error('Token refresh failed');
    }

    const tokenData = await response.json();
    this.updateTokens(tokenData);
  }

  updateTokens(tokenData) {
    this.accessToken = tokenData.access_token;
    this.refreshToken = tokenData.refresh_token || this.refreshToken;
    this.expiresAt = Date.now() + (tokenData.expires_in * 1000) - 60000; // 1 min buffer
  }
}

Error Handling

Implement Comprehensive Error Handling

class APIClient {
  async makeRequest(endpoint, options = {}) {
    const maxRetries = 3;
    let retries = 0;

    while (retries < maxRetries) {
      try {
        const response = await fetch(`https://getbill.io/external-api/v1${endpoint}`, {
          ...options,
          headers: {
            'Authorization': `Bearer ${await this.getValidToken()}`,
            'Content-Type': 'application/json',
            ...options.headers
          }
        });

        // Handle different response codes
        if (response.ok) {
          return await response.json();
        }

        const error = await response.json();

        switch (response.status) {
          case 401:
            // Token expired, refresh and retry
            await this.refreshToken();
            retries++;
            continue;

          case 429:
            // Rate limited, wait and retry
            const retryAfter = response.headers.get('Retry-After') || Math.pow(2, retries);
            await this.sleep(retryAfter * 1000);
            retries++;
            continue;

          case 500:
          case 502:
          case 503:
            // Server error, retry with backoff
            if (retries < maxRetries - 1) {
              await this.sleep(Math.pow(2, retries) * 1000);
              retries++;
              continue;
            }
            break;

          default:
            // Client error, don't retry
            throw new APIError(error.message, response.status, error.details);
        }

        throw new APIError(error.message, response.status, error.details);

      } catch (error) {
        if (retries === maxRetries - 1) {
          throw error;
        }
        
        // Network error, retry with backoff
        await this.sleep(Math.pow(2, retries) * 1000);
        retries++;
      }
    }
  }

  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

class APIError extends Error {
  constructor(message, status, details = null) {
    super(message);
    this.name = 'APIError';
    this.status = status;
    this.details = details;
  }
}

Rate Limiting & Performance

Respect Rate Limits

class RateLimitedClient {
  constructor() {
    this.requestQueue = [];
    this.processing = false;
    this.rateLimitRemaining = null;
    this.rateLimitReset = null;
  }

  async enqueueRequest(requestFn) {
    return new Promise((resolve, reject) => {
      this.requestQueue.push({ requestFn, resolve, reject });
      this.processQueue();
    });
  }

  async processQueue() {
    if (this.processing || this.requestQueue.length === 0) return;
    
    this.processing = true;

    while (this.requestQueue.length > 0) {
      // Check rate limit
      if (this.rateLimitRemaining !== null && this.rateLimitRemaining < 10) {
        const waitTime = this.getWaitTime();
        if (waitTime > 0) {
          await this.sleep(waitTime);
        }
      }

      const { requestFn, resolve, reject } = this.requestQueue.shift();
      
      try {
        const response = await requestFn();
        this.updateRateLimitInfo(response);
        resolve(response);
      } catch (error) {
        reject(error);
      }

      // Small delay between requests
      await this.sleep(100);
    }

    this.processing = false;
  }

  updateRateLimitInfo(response) {
    this.rateLimitRemaining = parseInt(response.headers.get('X-RateLimit-Remaining')) || null;
    this.rateLimitReset = parseInt(response.headers.get('X-RateLimit-Reset')) || null;
  }

  getWaitTime() {
    if (!this.rateLimitReset) return 0;
    
    const now = Math.floor(Date.now() / 1000);
    return Math.max(0, (this.rateLimitReset - now) * 1000);
  }
}

Optimize Pagination

// ✅ Good: Efficient pagination
async function getAllDebts(client) {
  const allDebts = [];
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const response = await client.getDebts({ 
      page, 
      limit: 100 // Use larger page sizes
    });
    
    allDebts.push(...response.data);
    hasMore = page < response.pagination.pages;
    page++;

    // Add small delay to be nice to the API
    if (hasMore) {
      await new Promise(resolve => setTimeout(resolve, 100));
    }
  }

  return allDebts;
}

// ❌ Bad: Many small requests
async function getAllDebtsInefficient(client) {
  const allDebts = [];
  
  for (let page = 1; page <= 50; page++) {
    const response = await client.getDebts({ page, limit: 10 });
    allDebts.push(...response.data);
  }
  
  return allDebts;
}

Data Management

Caching Strategy

class CachedAPIClient {
  constructor(apiClient, cacheTimeout = 300000) { // 5 minutes
    this.apiClient = apiClient;
    this.cache = new Map();
    this.cacheTimeout = cacheTimeout;
  }

  async getCachedData(key, fetchFn) {
    const cached = this.cache.get(key);
    
    if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
      return cached.data;
    }

    const data = await fetchFn();
    this.cache.set(key, {
      data,
      timestamp: Date.now()
    });

    return data;
  }

  async getCompanyProfile() {
    return this.getCachedData('company:profile', () => 
      this.apiClient.makeRequest('/company/profile')
    );
  }

  async getDebt(id) {
    return this.getCachedData(`debt:${id}`, () => 
      this.apiClient.makeRequest(`/debts/${id}`)
    );
  }

  invalidateCache(pattern) {
    for (const key of this.cache.keys()) {
      if (key.includes(pattern)) {
        this.cache.delete(key);
      }
    }
  }

  // Invalidate cache when data is modified
  async updateDebt(id, data) {
    const result = await this.apiClient.makeRequest(`/debts/${id}`, {
      method: 'PUT',
      body: JSON.stringify(data)
    });
    
    this.invalidateCache(`debt:${id}`);
    this.invalidateCache('debts:list');
    
    return result;
  }
}

Validate Data Before Sending

class DebtValidator {
  static validate(debtData) {
    const errors = {};

    // Required fields
    if (!debtData.firstname?.trim()) {
      errors.firstname = 'First name is required';
    }

    if (!debtData.lastname?.trim()) {
      errors.lastname = 'Last name is required';
    }

    if (!debtData.phone?.trim()) {
      errors.phone = 'Phone number is required';
    }

    if (!debtData.amount || debtData.amount <= 0) {
      errors.amount = 'Amount must be greater than 0';
    }

    if (!debtData.currency?.trim()) {
      errors.currency = 'Currency is required';
    }

    // Format validation
    if (debtData.email && !this.isValidEmail(debtData.email)) {
      errors.email = 'Invalid email format';
    }

    if (debtData.birthdate && !this.isValidDate(debtData.birthdate)) {
      errors.birthdate = 'Invalid date format (use YYYY-MM-DD)';
    }

    return {
      isValid: Object.keys(errors).length === 0,
      errors
    };
  }

  static isValidEmail(email) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
  }

  static isValidDate(dateString) {
    return /^\d{4}-\d{2}-\d{2}$/.test(dateString) && !isNaN(Date.parse(dateString));
  }
}

// Usage
async function createDebt(client, debtData) {
  const validation = DebtValidator.validate(debtData);
  
  if (!validation.isValid) {
    throw new ValidationError('Invalid debt data', validation.errors);
  }

  return client.makeRequest('/debts', {
    method: 'POST',
    body: JSON.stringify(debtData)
  });
}

Logging & Monitoring

Implement Comprehensive Logging

class APILogger {
  constructor(logLevel = 'info') {
    this.logLevel = logLevel;
    this.levels = { error: 0, warn: 1, info: 2, debug: 3 };
  }

  log(level, message, context = {}) {
    if (this.levels[level] <= this.levels[this.logLevel]) {
      console.log(JSON.stringify({
        timestamp: new Date().toISOString(),
        level,
        message,
        context
      }));
    }
  }

  logAPICall(endpoint, method, duration, status, error = null) {
    const context = {
      endpoint,
      method,
      duration,
      status,
      success: status >= 200 && status < 300
    };

    if (error) {
      context.error = error.message;
      this.log('error', `API call failed: ${method} ${endpoint}`, context);
    } else {
      this.log('info', `API call: ${method} ${endpoint}`, context);
    }
  }

  logRateLimit(remaining, reset) {
    this.log('info', 'Rate limit status', { remaining, reset });
    
    if (remaining < 50) {
      this.log('warn', 'Rate limit running low', { remaining, reset });
    }
  }
}

// Usage with API client
class MonitoredAPIClient extends APIClient {
  constructor() {
    super();
    this.logger = new APILogger();
  }

  async makeRequest(endpoint, options = {}) {
    const startTime = Date.now();
    const method = options.method || 'GET';
    
    try {
      const response = await super.makeRequest(endpoint, options);
      const duration = Date.now() - startTime;
      
      this.logger.logAPICall(endpoint, method, duration, response.status || 200);
      
      // Log rate limit info
      if (response.headers) {
        const remaining = response.headers.get('X-RateLimit-Remaining');
        const reset = response.headers.get('X-RateLimit-Reset');
        if (remaining !== null) {
          this.logger.logRateLimit(parseInt(remaining), parseInt(reset));
        }
      }
      
      return response;
    } catch (error) {
      const duration = Date.now() - startTime;
      this.logger.logAPICall(endpoint, method, duration, error.status || 0, error);
      throw error;
    }
  }
}

Testing Your Integration

Unit Testing Example

// Mock API client for testing
class MockAPIClient {
  constructor() {
    this.responses = new Map();
  }

  mockResponse(endpoint, response) {
    this.responses.set(endpoint, response);
  }

  async makeRequest(endpoint) {
    const response = this.responses.get(endpoint);
    if (!response) {
      throw new Error(`No mock response for ${endpoint}`);
    }
    return response;
  }
}

// Test example
describe('Debt Service', () => {
  let debtService;
  let mockClient;

  beforeEach(() => {
    mockClient = new MockAPIClient();
    debtService = new DebtService(mockClient);
  });

  it('should create a debt successfully', async () => {
    const debtData = {
      firstname: 'John',
      lastname: 'Doe',
      phone: '+33123456789',
      amount: 1000,
      currency: 'EUR'
    };

    mockClient.mockResponse('/debts', {
      error: false,
      data: { id: 'abc123', ...debtData }
    });

    const result = await debtService.createDebt(debtData);
    expect(result.data.id).toBe('abc123');
  });

  it('should handle validation errors', async () => {
    const invalidData = {
      firstname: '',
      amount: -100
    };

    await expect(debtService.createDebt(invalidData))
      .rejects
      .toThrow('Invalid debt data');
  });
});

Performance Optimization

Batch Operations

Group multiple operations together when possible to reduce API calls.

Parallel Requests

Make independent requests in parallel rather than sequentially.

Efficient Filtering

Use API filters to reduce data transfer and processing time.

Pagination Optimization

Use appropriate page sizes and implement efficient pagination logic.

Parallel Processing Example

// ✅ Good: Parallel requests
async function getDebtDetails(client, debtIds) {
  const promises = debtIds.map(id => client.getDebt(id));
  return Promise.all(promises);
}

// ✅ Good: Parallel with error handling
async function getDebtDetailsWithErrorHandling(client, debtIds) {
  const promises = debtIds.map(async (id) => {
    try {
      return await client.getDebt(id);
    } catch (error) {
      console.error(`Failed to get debt ${id}:`, error.message);
      return null;
    }
  });
  
  const results = await Promise.all(promises);
  return results.filter(debt => debt !== null);
}

// ❌ Bad: Sequential requests
async function getDebtDetailsSequential(client, debtIds) {
  const debts = [];
  for (const id of debtIds) {
    const debt = await client.getDebt(id);
    debts.push(debt);
  }
  return debts;
}

Security Considerations

Input Sanitization

function sanitizeInput(input) {
  if (typeof input !== 'string') return input;
  
  return input
    .trim()
    .replace(/[<>]/g, '') // Remove potential HTML tags
    .substring(0, 1000); // Limit length
}

function sanitizeDebtData(data) {
  return {
    ...data,
    firstname: sanitizeInput(data.firstname),
    lastname: sanitizeInput(data.lastname),
    email: sanitizeInput(data.email),
    address: sanitizeInput(data.address),
    object: sanitizeInput(data.object)
  };
}

Environment Configuration

// config.js
const config = {
  development: {
    apiUrl: 'https://api-dev.getbill.io',
    clientId: process.env.GETBILL_CLIENT_ID_DEV,
    clientSecret: process.env.GETBILL_CLIENT_SECRET_DEV,
    logLevel: 'debug'
  },
  production: {
    apiUrl: 'https://getbill.io',
    clientId: process.env.GETBILL_CLIENT_ID,
    clientSecret: process.env.GETBILL_CLIENT_SECRET,
    logLevel: 'info'
  }
};

const environment = process.env.NODE_ENV || 'development';
module.exports = config[environment];
By following these best practices, you’ll build a robust, efficient, and maintainable integration with the GetBill API. Remember to always test thoroughly and monitor your integration’s performance in production.