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
- Swagger UI: https://referentiel.staging.ubsi.fr/api/swagger
- Documentation complète: Voir Référence API
- Schémas de données: Voir Schémas et Enums
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