Bonnes pratiques d'API REST pour les données d'entreprises
KVKBase Team

Bonnes pratiques d'API REST pour les données d'entreprises

Patterns prêts pour la production : cache, nouvelles tentatives, limitation de débit, disjoncteurs et normalisation des réponses.

apibonnes-pratiquesarchitecture

Appeler une API de données d’entreprises en développement est facile. La rendre fiable en production, c’est là que le vrai travail commence. Pannes réseau, limites de débit, indisponibilités en amont et caches périmés peuvent tous dégrader l’expérience de vos utilisateurs si vous ne les gérez pas correctement.

Cet article présente des patterns éprouvés en production pour intégrer à votre application des API de données d’entreprises comme KVKBase, l’API KVK néerlandaise ou le service européen VIES.

1. Un cache intelligent

Les données d’entreprise changent peu. Le nom, l’adresse et le numéro KVK d’une entreprise restent stables pendant des mois, voire des années. Un cache agressif est donc à la fois sûr et précieux.

Valeurs de TTL recommandées

Type de donnéeTTLJustification
Profil d’une entreprise active24 heuresLes adresses ou les noms changent rarement du jour au lendemain
Entreprise inactive ou dissoute7 joursLe statut ne reviendra pas en arrière
Résultats de recherche1 heureDe nouvelles entreprises s’immatriculent chaque jour
Recherches échouées (404)1 heureL’entreprise vient peut-être de s’immatriculer
Validation VIES (valide)7 joursLe statut TVA change rarement
Validation VIES (invalide)1 heureIl peut s’agir d’une indisponibilité temporaire de VIES

Implémentation

class CompanyDataCache {
  constructor(store) {
    this.store = store; // Redis, Memcached ou en mémoire
  }

  async get(key) {
    const entry = await this.store.get(key);
    if (!entry) return null;

    const parsed = JSON.parse(entry);
    if (Date.now() > parsed.expiresAt) {
      await this.store.delete(key);
      return null;
    }

    return parsed.data;
  }

  async set(key, data, ttlSeconds) {
    const entry = {
      data,
      expiresAt: Date.now() + (ttlSeconds * 1000),
      cachedAt: Date.now()
    };

    await this.store.set(key, JSON.stringify(entry), ttlSeconds);
  }

  ttlForLookup(result) {
    if (!result) return 3600;                // Introuvable : 1 heure
    if (!result.isActive) return 604800;     // Inactive : 7 jours
    return 86400;                            // Active : 24 heures
  }
}

Conception des clés de cache

Utilisez des clés de cache structurées et prévisibles :

function cacheKey(operation, ...parts) {
  return `company:${operation}:${parts.join(':')}`;
}

// Exemples :
// company:lookup:12345678
// company:search:bol.com:limit=10
// company:vies:NL:123456789B01

2. Nouvelles tentatives avec backoff exponentiel

Les défaillances transitoires arrivent. Une stratégie de nouvelles tentatives avec backoff exponentiel les gère proprement sans submerger le service en amont.

async function withRetry(fn, options = {}) {
  const {
    maxRetries = 3,
    baseDelay = 1000,
    maxDelay = 30000,
    retryableErrors = [502, 503, 504, 429]
  } = options;

  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      const isRetryable = error.status
        ? retryableErrors.includes(error.status)
        : error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT';

      if (!isRetryable || attempt === maxRetries) {
        throw error;
      }

      const delay = Math.min(
        baseDelay * Math.pow(2, attempt) + Math.random() * 1000,
        maxDelay
      );

      // En cas de limitation de débit, utiliser l'en-tête Retry-After
      if (error.status === 429 && error.retryAfter) {
        await sleep(error.retryAfter * 1000);
      } else {
        await sleep(delay);
      }
    }
  }
}

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

Utilisation

const result = await withRetry(
  () => kvkbase.lookup('12345678'),
  { maxRetries: 3, baseDelay: 1000 }
);

3. Gestion des limites de débit

La plupart des API de données d’entreprises imposent des limites de débit. Bien les gérer signifie que votre application se dégrade en douceur au lieu de planter.

Lire les en-têtes de limite de débit

class RateLimitAwareClient {
  constructor(baseUrl, apiKey) {
    this.baseUrl = baseUrl;
    this.apiKey = apiKey;
    this.remaining = Infinity;
    this.resetAt = 0;
  }

  async request(path) {
    // Attendre de façon proactive si l'on sait que le quota est épuisé
    if (this.remaining <= 0 && Date.now() < this.resetAt) {
      const waitTime = this.resetAt - Date.now();
      await sleep(waitTime);
    }

    const response = await fetch(`${this.baseUrl}${path}`, {
      headers: { 'Authorization': `Bearer ${this.apiKey}` }
    });

    // Mettre à jour l'état de la limite de débit à partir des en-têtes de réponse
    this.remaining = parseInt(response.headers.get('X-RateLimit-Remaining') || 'Infinity');
    this.resetAt = parseInt(response.headers.get('X-RateLimit-Reset') || '0') * 1000;

    if (response.status === 429) {
      const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
      throw { status: 429, retryAfter, message: 'Rate limited' };
    }

    if (!response.ok) {
      throw { status: response.status, message: response.statusText };
    }

    return response.json();
  }
}

File d’attente des requêtes pour les opérations en masse

Pour traiter de nombreuses recherches, utilisez une file d’attente afin de rester dans les limites de débit :

class RequestQueue {
  constructor(requestsPerSecond = 5) {
    this.interval = 1000 / requestsPerSecond;
    this.lastRequest = 0;
    this.queue = [];
    this.processing = false;
  }

  async enqueue(fn) {
    return new Promise((resolve, reject) => {
      this.queue.push({ fn, resolve, reject });
      this.process();
    });
  }

  async process() {
    if (this.processing) return;
    this.processing = true;

    while (this.queue.length > 0) {
      const elapsed = Date.now() - this.lastRequest;
      if (elapsed < this.interval) {
        await sleep(this.interval - elapsed);
      }

      const { fn, resolve, reject } = this.queue.shift();
      this.lastRequest = Date.now();

      try {
        resolve(await fn());
      } catch (error) {
        reject(error);
      }
    }

    this.processing = false;
  }
}

4. Disjoncteur (circuit breaker) pour les services externes

Le service de validation de TVA VIES est connu pour ses pannes intermittentes. Un disjoncteur évite à votre application de perdre du temps sur des requêtes vouées à l’échec.

class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.resetTimeout = options.resetTimeout || 60000;
    this.failures = 0;
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.lastFailure = null;
  }

  async execute(fn) {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailure > this.resetTimeout) {
        this.state = 'HALF_OPEN';
      } else {
        throw new Error('Circuit breaker is OPEN');
      }
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  onSuccess() {
    this.failures = 0;
    this.state = 'CLOSED';
  }

  onFailure() {
    this.failures++;
    this.lastFailure = Date.now();
    if (this.failures >= this.failureThreshold) {
      this.state = 'OPEN';
    }
  }
}

// Utilisation avec VIES
const viesBreaker = new CircuitBreaker({
  failureThreshold: 3,
  resetTimeout: 120000 // 2 minutes
});

async function validateVat(countryCode, vatNumber) {
  try {
    return await viesBreaker.execute(() =>
      viesClient.validate(countryCode, vatNumber)
    );
  } catch (error) {
    if (error.message === 'Circuit breaker is OPEN') {
      return { valid: null, message: 'VAT validation temporarily unavailable' };
    }
    throw error;
  }
}

5. Normalisation des réponses

Les différentes API de données d’entreprises renvoient les données dans des formats différents. Normalisez les réponses dans un format interne cohérent :

function normalizeCompanyData(source, rawData) {
  switch (source) {
    case 'kvkbase':
      return {
        kvkNumber: rawData.kvkNumber,
        name: rawData.tradeName,
        legalForm: rawData.legalForm,
        address: {
          street: rawData.address?.street,
          houseNumber: rawData.address?.houseNumber,
          postalCode: rawData.address?.postalCode,
          city: rawData.address?.city,
          country: 'NL'
        },
        vatNumber: rawData.vatNumber,
        isActive: rawData.isActive,
        source: 'kvkbase',
        retrievedAt: new Date().toISOString()
      };

    case 'kvk-official':
      return {
        kvkNumber: rawData.kvkNummer,
        name: rawData.naam,
        legalForm: rawData.rechtsvorm,
        address: {
          street: rawData.straatnaam,
          houseNumber: rawData.huisnummer,
          postalCode: rawData.postcode,
          city: rawData.plaats,
          country: 'NL'
        },
        vatNumber: null, // L'API KVK officielle n'inclut pas la TVA
        isActive: rawData.actief === 'Ja',
        source: 'kvk-official',
        retrievedAt: new Date().toISOString()
      };

    default:
      throw new Error(`Unknown source: ${source}`);
  }
}

6. Timeouts et solutions de repli

Définissez des timeouts explicites et un comportement de repli :

async function lookupWithFallback(kvkNumber) {
  // Essayer la source principale avec un timeout
  try {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), 5000);

    const result = await fetch(
      `https://api.kvkbase.nl/api/v1/lookup/${kvkNumber}`,
      {
        headers: { 'Authorization': `Bearer ${API_KEY}` },
        signal: controller.signal
      }
    );

    clearTimeout(timeout);
    return await result.json();
  } catch (error) {
    // Se rabattre sur le cache, même périmé
    const staleCache = await cache.getStale(kvkNumber);
    if (staleCache) {
      return { ...staleCache, _stale: true };
    }

    throw error;
  }
}

Tout assembler

Un client de données d’entreprises de niveau production combine tous ces patterns :

class CompanyDataService {
  constructor(config) {
    this.client = new RateLimitAwareClient(config.baseUrl, config.apiKey);
    this.cache = new CompanyDataCache(config.cacheStore);
    this.queue = new RequestQueue(config.requestsPerSecond);
    this.viesBreaker = new CircuitBreaker({ failureThreshold: 3 });
  }

  async lookup(kvkNumber) {
    const cached = await this.cache.get(`lookup:${kvkNumber}`);
    if (cached) return cached;

    const result = await this.queue.enqueue(() =>
      withRetry(() => this.client.request(`/lookup/${kvkNumber}`))
    );

    const ttl = this.cache.ttlForLookup(result);
    await this.cache.set(`lookup:${kvkNumber}`, result, ttl);

    return result;
  }
}

Ces patterns ne sont pas propres aux données KVK : ils s’appliquent à toute intégration d’API externe. Mais les recherches de données d’entreprises, avec leur mélange de données stables et de services en amont peu fiables, se prêtent particulièrement bien à ce niveau d’ingénierie de résilience.