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.
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ée | TTL | Justification |
|---|---|---|
| Profil d’une entreprise active | 24 heures | Les adresses ou les noms changent rarement du jour au lendemain |
| Entreprise inactive ou dissoute | 7 jours | Le statut ne reviendra pas en arrière |
| Résultats de recherche | 1 heure | De nouvelles entreprises s’immatriculent chaque jour |
| Recherches échouées (404) | 1 heure | L’entreprise vient peut-être de s’immatriculer |
| Validation VIES (valide) | 7 jours | Le statut TVA change rarement |
| Validation VIES (invalide) | 1 heure | Il 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.