Aller au contenu principal

Guide d'intégration de l'API Référentiel

Ce guide vous accompagne dans l'intégration de l'API Référentiel UBSI dans vos applications.

Démarrage rapide

1. Configuration de base

// Configuration API
const API_BASE_URL = 'https://referentiel.staging.ubsi.fr/api/publique';

// Headers recommandés
const headers = {
'Content-Type': 'application/json',
Accept: 'application/json',
};

// Fonction helper
async function apiRequest(endpoint, options = {}) {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers: { ...headers, ...options.headers },
});

if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'API Error');
}

return response.json();
}

2. Premiers appels

// Récupérer tous les produits (simple)
const produits = await apiRequest('/produits');

// Récupérer des produits avec pagination
const page1 = await apiRequest('/produits?page=1&limit=20');

// Rechercher des produits
const results = await apiRequest(
'/produits?search=samsung&categories=telephonie'
);

// Récupérer un produit spécifique avec ses fournisseurs
const produit = await apiRequest('/produits/123?include=fournisseurs');

Cas d'usage courants

Synchronisation de catalogue produits

Scénario: Vous souhaitez synchroniser votre application avec le référentiel de produits.

// 1. Récupération initiale (pagination pour gros volumes)
async function syncAllProducts() {
let page = 1;
const limit = 100;
const allProducts = [];

while (true) {
const response = await apiRequest(`/produits?page=${page}&limit=${limit}`);
allProducts.push(...response.rows);

if (page >= response.totalPages) break;
page++;
}

return allProducts;
}

// 2. Synchronisation incrémentale (produits actifs uniquement)
async function syncActiveProducts() {
return await apiRequest(
'/produits?statutProduit=actif&fields=id,ean,nom,prixDeVenteCents,categories,marque'
);
}

// 3. Détecter les changements (utiliser updatedAt si disponible)
async function getRecentlyUpdated(since) {
const filters = JSON.stringify([
{ field: 'updatedAt', operator: '>', value: since },
]);

return await apiRequest(`/produits?filters=${encodeURIComponent(filters)}`);
}

Gestion des prix fournisseurs

Scénario: Calculer le meilleur prix d'achat pour un produit.

async function getBestSupplierPrice(produitId) {
const response = await apiRequest(
`/produits/${produitId}/fournisseurs?sort=prixFournisseurCents&order=asc&limit=1`
);

if (response.rows.length === 0) {
throw new Error('Aucun fournisseur pour ce produit');
}

return {
fournisseur: response.rows[0],
prixEnEuros: response.rows[0].prixFournisseurCents / 100,
};
}

// Comparer tous les fournisseurs d'un produit
async function compareSuppliers(produitId) {
const response = await apiRequest(`/produits/${produitId}/fournisseurs`);

return response.rows
.filter((f) => f.statutRelation === 'actif')
.map((f) => ({
fournisseurId: f.fournisseurId,
nomFournisseur: f.fournisseur?.nomLegal,
prixEuros: f.prixFournisseurCents / 100,
delaiJours: f.delaiLivraisonJours,
quantiteMin: f.quantiteMinimaleCommande,
reference: f.referenceFournisseur,
}))
.sort((a, b) => a.prixEuros - b.prixEuros);
}

Recherche avancée multi-critères

Scénario: Interface de recherche utilisateur avec filtres.

async function searchProducts(criteria) {
const {
query, // Texte libre
categories, // Array de catégories
marque,
prixMin,
prixMax,
statut,
page = 1,
limit = 20,
} = criteria;

// Construction des filtres
const filters = [];

if (categories && categories.length > 0) {
filters.push({
field: 'categories',
operator: 'isAnyOf',
value: categories,
});
}

if (marque) {
filters.push({ field: 'marque', operator: 'equals', value: marque });
}

if (prixMin !== undefined) {
filters.push({
field: 'prixDeVenteCents',
operator: '>=',
value: prixMin * 100,
});
}

if (prixMax !== undefined) {
filters.push({
field: 'prixDeVenteCents',
operator: '<=',
value: prixMax * 100,
});
}

if (statut) {
filters.push({ field: 'statutProduit', operator: 'equals', value: statut });
}

// Construction de l'URL
const params = new URLSearchParams({
page,
limit,
...(query && { search: query }),
...(filters.length > 0 && { filters: JSON.stringify(filters) }),
});

return await apiRequest(`/produits?${params}`);
}

// Exemple d'utilisation
const results = await searchProducts({
query: 'smartphone',
categories: ['telephonie'],
prixMin: 200,
prixMax: 1000,
statut: 'actif',
page: 1,
limit: 20,
});

Mise à jour en lot

Scénario: Mise à jour de prix suite à une négociation fournisseur.

async function updateProductPrices(updates) {
// updates = [{ id: 1, prixDeVenteCents: 79900 }, ...]

// Validation
if (updates.length === 0 || updates.length > 500) {
throw new Error('Le nombre de mises à jour doit être entre 1 et 500');
}

const response = await apiRequest('/produits', {
method: 'PATCH',
body: JSON.stringify({ produits: updates }),
});

// Vérification des résultats
const errors = response.resultats.filter((r) => r.statut !== 'ok');

if (errors.length > 0) {
console.warn(
`${errors.length} produits n'ont pas pu être mis à jour:`,
errors
);
}

return {
success: response.succes,
updated: response.modifies,
errors,
};
}

// Exemple : Appliquer une remise de 10% sur tous les produits informatique
async function applyDiscountToCategory(category, discountPercent) {
// 1. Récupérer les produits de la catégorie
const products = await apiRequest(
`/produits?categories=${category}&fields=id,prixDeVenteCents`
);

// 2. Calculer les nouveaux prix
const updates = products.map((p) => ({
id: p.id,
prixDeVenteCents: Math.round(
p.prixDeVenteCents * (1 - discountPercent / 100)
),
}));

// 3. Appliquer les mises à jour par lots de 500
const results = [];
for (let i = 0; i < updates.length; i += 500) {
const batch = updates.slice(i, i + 500);
const result = await updateProductPrices(batch);
results.push(result);
}

return results;
}

Import de données

Scénario: Importer un catalogue depuis un fichier externe.

// Import via fichier JSON
async function importProducts(file) {
const formData = new FormData();
formData.append('file', file);

const response = await fetch(`${API_BASE_URL}/produits/import`, {
method: 'POST',
body: formData,
// Ne pas mettre Content-Type, le navigateur le fera automatiquement
});

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

return await response.json();
}

// Préparation de données pour import
function prepareProductsForImport(rawData) {
return rawData.map((item) => ({
ean: item.ean || item.barcode,
nom: item.name,
prixDeVenteCents: Math.round(item.price * 100),
description: item.description || '',
categories: item.category || 'accessoires',
marque: item.brand || '',
statutProduit: item.active ? 'actif' : 'inactif',
photosUrls: item.images || [],
poids: item.weight ? `${item.weight}kg` : undefined,
dimensions: item.dimensions || undefined,
}));
}

// Node.js : Import depuis fichier local
async function importFromFile(filePath) {
const fs = require('fs');
const FormData = require('form-data');

const form = new FormData();
form.append('file', fs.createReadStream(filePath));

const response = await fetch(`${API_BASE_URL}/produits/import`, {
method: 'POST',
body: form,
headers: form.getHeaders(),
});

return await response.json();
}

Gestion des stocks et disponibilité

Scénario: Vérifier la disponibilité d'un produit dans les entrepôts.

async function checkProductAvailability(ean) {
// 1. Trouver le produit par EAN
const filters = JSON.stringify([
{ field: 'ean', operator: 'equals', value: ean },
]);

const products = await apiRequest(
`/produits?filters=${encodeURIComponent(filters)}`
);

if (products.length === 0) {
throw new Error('Produit non trouvé');
}

const produit = products[0];

// 2. Récupérer les fournisseurs actifs
const fournisseurs = await apiRequest(`/produits/${produit.id}/fournisseurs`);

const activeFournisseurs = fournisseurs.rows.filter(
(f) => f.statutRelation === 'actif' || f.statutRelation === 'prefere'
);

// 3. Vérifier les entrepôts selon la catégorie
let entrepotsCompatibles = await apiRequest('/entrepots?statut=operationnel');

// Filtrer par type si nécessaire
if (produit.categories === 'electromenager') {
const filters = JSON.stringify([
{
field: 'typeEntrepot',
operator: 'isAnyOf',
value: ['electromenager', 'generaliste'],
},
]);
entrepotsCompatibles = await apiRequest(
`/entrepots?filters=${encodeURIComponent(filters)}&statut=operationnel`
);
}

return {
produit: {
id: produit.id,
nom: produit.nom,
ean: produit.ean,
statut: produit.statutProduit,
},
disponible:
produit.statutProduit === 'actif' && activeFournisseurs.length > 0,
fournisseurs: activeFournisseurs.length,
entrepotsDisponibles: entrepotsCompatibles.length,
delaiMoyenLivraison:
activeFournisseurs.length > 0
? Math.round(
activeFournisseurs.reduce(
(sum, f) => sum + (f.delaiLivraisonJours || 0),
0
) / activeFournisseurs.length
)
: null,
};
}

Recherche de magasins avec services

Scénario: Trouver les magasins offrant un service spécifique.

async function findStoresWithService(service, ville) {
const filters = [{ field: 'statut', operator: 'equals', value: 'ouvert' }];

// Recherche par ville dans l'adresse
let params = new URLSearchParams({
filters: JSON.stringify(filters),
search: ville,
});

const magasins = await apiRequest(`/magasins?${params}`);

// Filtrer par service disponible
return magasins.filter(
(m) => m.servicesDisponibles && m.servicesDisponibles.includes(service)
);
}

// Exemple : Trouver tous les megastores avec click & collect à Lyon
const stores = await findStoresWithService('click_collect', 'Lyon');
const megastores = stores.filter((s) => s.type === 'megastore');

Gestion des erreurs

Stratégie de gestion

class ApiError extends Error {
constructor(statusCode, message, details) {
super(message);
this.statusCode = statusCode;
this.details = details;
}
}

async function robustApiRequest(endpoint, options = {}) {
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers: { ...headers, ...options.headers },
});

if (!response.ok) {
const error = await response.json();
throw new ApiError(response.status, error.message || 'API Error', error);
}

return await response.json();
} catch (error) {
if (error instanceof ApiError) {
// Erreurs API structurées
switch (error.statusCode) {
case 400:
console.error('Requête invalide:', error.message);
break;
case 404:
console.error('Ressource non trouvée:', error.message);
break;
case 422:
console.error('Erreur métier:', error.message, error.details);
break;
case 500:
console.error('Erreur serveur:', error.message);
break;
default:
console.error('Erreur API:', error);
}
throw error;
} else {
// Erreurs réseau ou autres
console.error('Erreur de connexion:', error);
throw new Error("Impossible de contacter l'API");
}
}
}

Retry avec backoff

async function retryableRequest(endpoint, options = {}, maxRetries = 3) {
let lastError;

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await robustApiRequest(endpoint, options);
} catch (error) {
lastError = error;

// Ne pas retry sur 400, 404, 422
if (error.statusCode && error.statusCode < 500) {
throw error;
}

// Attendre avant de retenter (exponential backoff)
if (attempt < maxRetries - 1) {
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}

throw lastError;
}

Performance et optimisation

Mise en cache

class ApiCache {
constructor(ttl = 300000) {
// TTL par défaut: 5 minutes
this.cache = new Map();
this.ttl = ttl;
}

set(key, value) {
this.cache.set(key, {
value,
expires: Date.now() + this.ttl,
});
}

get(key) {
const item = this.cache.get(key);
if (!item) return null;

if (Date.now() > item.expires) {
this.cache.delete(key);
return null;
}

return item.value;
}

clear() {
this.cache.clear();
}
}

const cache = new ApiCache();

async function cachedApiRequest(endpoint, options = {}) {
// Ne cacher que les GET
if (options.method && options.method !== 'GET') {
return await apiRequest(endpoint, options);
}

const cacheKey = `${endpoint}${JSON.stringify(options)}`;
const cached = cache.get(cacheKey);

if (cached) {
console.log('Cache hit:', endpoint);
return cached;
}

const result = await apiRequest(endpoint, options);
cache.set(cacheKey, result);

return result;
}

Requêtes parallèles

// Charger plusieurs ressources en parallèle
async function loadDashboardData() {
const [produits, fournisseurs, entrepots, magasins] = await Promise.all([
apiRequest('/produits?limit=10&sort=-createdAt'),
apiRequest('/fournisseurs?statut=actif&limit=10'),
apiRequest('/entrepots?statut=operationnel'),
apiRequest('/magasins?statut=ouvert&limit=5'),
]);

return {
recentProducts: produits.rows,
activeSuppliers: fournisseurs.rows,
warehouses: entrepots,
openStores: magasins.rows,
};
}

Sélection de champs

// Charger uniquement les champs nécessaires
async function getProductSummaries() {
return await apiRequest(
'/produits?fields=id,ean,nom,prixDeVenteCents,statutProduit&limit=100'
);
}

// Au lieu de
async function getFullProducts() {
// Charge tous les champs inutiles → Plus lent, plus de bande passante
return await apiRequest('/produits?limit=100');
}

Bonnes pratiques

1. Valider les données avant envoi

function validateProduct(product) {
const errors = [];

if (!product.ean || !/^\d{13}$/.test(product.ean)) {
errors.push('EAN doit être un code à 13 chiffres');
}

if (!product.nom || product.nom.length < 3) {
errors.push('Le nom doit contenir au moins 3 caractères');
}

if (product.prixDeVenteCents < 0) {
errors.push('Le prix ne peut pas être négatif');
}

if (errors.length > 0) {
throw new Error(`Validation échouée: ${errors.join(', ')}`);
}

return true;
}

2. Gérer la pagination correctement

// ❌ Mauvais : Charger toutes les pages d'un coup
async function getAllProductsBad() {
const allProducts = [];
let page = 1;

while (true) {
const response = await apiRequest(`/produits?page=${page}&limit=100`);
allProducts.push(...response.rows);

if (page >= response.totalPages) break;
page++;
// Problème: Peut charger des milliers de produits en mémoire
}

return allProducts;
}

// ✅ Bon : Traiter par batch
async function processAllProducts(processFn) {
let page = 1;
const limit = 100;

while (true) {
const response = await apiRequest(`/produits?page=${page}&limit=${limit}`);

// Traiter chaque batch
await processFn(response.rows);

if (page >= response.totalPages) break;
page++;
}
}

3. Utiliser les filtres métier plutôt que filtrer côté client

// ❌ Mauvais : Charger tout puis filtrer
const allProducts = await apiRequest('/produits');
const samsungProducts = allProducts.filter((p) => p.marque === 'Samsung');

// ✅ Bon : Filtrer côté serveur
const samsungProducts = await apiRequest('/produits?marque=Samsung');

4. Gérer les soft-deletes

// Les entités supprimées ne sont plus visibles
const produit = await apiRequest('/produits/123'); // 404 si supprimé

// Pour vérifier l'existence avant suppression
async function safeDelete(id) {
try {
await apiRequest(`/produits/${id}`);
await apiRequest(`/produits/${id}`, { method: 'DELETE' });
return { success: true };
} catch (error) {
if (error.statusCode === 404) {
return { success: false, reason: 'already_deleted' };
}
throw error;
}
}

Monitoring et observabilité

Logging des requêtes

class ApiLogger {
static log(method, endpoint, duration, status) {
console.log(
JSON.stringify({
timestamp: new Date().toISOString(),
method,
endpoint,
duration,
status,
})
);
}
}

async function monitoredApiRequest(endpoint, options = {}) {
const start = Date.now();
const method = options.method || 'GET';

try {
const result = await apiRequest(endpoint, options);
ApiLogger.log(method, endpoint, Date.now() - start, 'success');
return result;
} catch (error) {
ApiLogger.log(method, endpoint, Date.now() - start, 'error');
throw error;
}
}

Métriques d'utilisation

class ApiMetrics {
constructor() {
this.requests = 0;
this.errors = 0;
this.totalDuration = 0;
}

record(duration, success) {
this.requests++;
this.totalDuration += duration;
if (!success) this.errors++;
}

getStats() {
return {
totalRequests: this.requests,
errors: this.errors,
errorRate:
this.requests > 0
? ((this.errors / this.requests) * 100).toFixed(2) + '%'
: '0%',
avgDuration:
this.requests > 0
? Math.round(this.totalDuration / this.requests) + 'ms'
: '0ms',
};
}
}

Exemples d'intégration par framework

React

import { useState, useEffect } from 'react';

function useProducts(filters = {}) {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
async function fetchProducts() {
try {
setLoading(true);
const params = new URLSearchParams(filters);
const data = await apiRequest(`/produits?${params}`);
setProducts(data.rows || data);
setError(null);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}

fetchProducts();
}, [JSON.stringify(filters)]);

return { products, loading, error };
}

// Utilisation
function ProductList() {
const { products, loading, error } = useProducts({
page: 1,
limit: 20,
statutProduit: 'actif',
});

if (loading) return <div>Chargement...</div>;
if (error) return <div>Erreur: {error}</div>;

return (
<ul>
{products.map((p) => (
<li key={p.id}>
{p.nom} - {p.prixDeVenteCents / 100}
</li>
))}
</ul>
);
}

Vue.js

import { ref, onMounted } from 'vue';

export function useProducts(filters) {
const products = ref([]);
const loading = ref(true);
const error = ref(null);

async function fetchProducts() {
try {
loading.value = true;
const params = new URLSearchParams(filters);
const data = await apiRequest(`/produits?${params}`);
products.value = data.rows || data;
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
}

onMounted(fetchProducts);

return { products, loading, error, refetch: fetchProducts };
}

Angular

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class ReferentielService {
private baseUrl = 'https://referentiel.staging.ubsi.fr/api/publique';

constructor(private http: HttpClient) {}

getProducts(filters?: any): Observable<any> {
const params = new URLSearchParams(filters);
return this.http.get(`${this.baseUrl}/produits?${params}`);
}

getProduct(id: number, include?: string): Observable<any> {
const url = include
? `${this.baseUrl}/produits/${id}?include=${include}`
: `${this.baseUrl}/produits/${id}`;
return this.http.get(url);
}

createProduct(product: any): Observable<any> {
return this.http.post(`${this.baseUrl}/produits`, product);
}
}

Support et ressources

Checklist d'intégration

  • Configuration de l'URL de base selon l'environnement
  • Gestion des erreurs HTTP (400, 404, 422, 500)
  • Implémentation du retry pour les erreurs 500
  • Validation des données avant envoi
  • Utilisation de la pagination pour gros volumes
  • Mise en cache des données statiques
  • Logging des requêtes pour debugging
  • Tests d'intégration avec l'API
  • Gestion du soft-delete dans la logique métier
  • Documentation du code d'intégration