L’idempotence dans les appels API : éviter les doublons avec Laravel et Vue.js
L'idempotence évite qu'une même intention métier produise plusieurs effets (par exemple deux commandes ou deux paiements) même si la requête est envoyée plusieurs fois. Cette notion s'appuie sur la sémantique HTTP et les pratiques d'API modernes.
Laravel 12 et 13 conservent une compatibilité ascendante et les patterns montrés ici fonctionnent sur les deux versions. Vue 3 utilise la Composition API ; Axios s'intègre via un composable ou provide/inject, méthode recommandée pour Vue 3.
Principe simple et pratique
L'approche courante : le client génère une clé d'idempotence (UUID) et l'envoie dans un header, typiquement X-Idempotency-Key. Le serveur stocke le résultat de la première exécution pour cette clé et renvoie la même réponse si la même clé arrive à nouveau. Ce pattern est très utile pour POSTs métier (création d'ordre, paiements, webhooks).
Pourquoi l'idempotence est cruciale en 2026
Avec la généralisation des microservices, des webhooks, des event-driven architectures et des paiements en ligne, les appels API dupliqués sont devenus un problème courant. Une mauvaise connexion réseau, un timeout côté client, ou un double-clic sur un bouton "Commander" peuvent entraîner des doublons de commandes, des paiements en double ou des inscriptions multiples. Stripe, PayPal et toutes les grandes plateformes de paiement imposent l'idempotence — et c'est une attente de plus en plus forte des consommateurs d'API.
Exemple compatible Laravel 12/13 (backend)
Crée une route API standard et un contrôleur qui vérifie la clé d'idempotence. Laravel 13 a introduit une meilleure gestion des attributs PHP et des middlewares, mais le code ci-dessous reste compatible avec les deux versions :
// routes/api.php
use App\Http\Controllers\OrderController;
Route::post('/orders', [OrderController::class, 'store']);// app/Http/Controllers/OrderController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
class OrderController extends Controller
{
public function store(Request $request)
{
$key = $request->header('X-Idempotency-Key');
if (!$key) {
return response()->json(['message' => 'Idempotency key required.'], 422);
}
$cacheKey = 'idempotency:' . $key;
if (Cache::has($cacheKey)) {
return response()->json(Cache::get($cacheKey), 200);
}
$data = $request->validate([
'product_id' => 'required|integer',
'quantity' => 'required|integer|min:1',
]);
$order = DB::transaction(function () use ($data) {
return \App\Models\Order::create([
'product_id' => $data['product_id'],
'quantity' => $data['quantity'],
'status' => 'pending',
]);
});
$response = ['message' => 'Order created successfully.', 'data' => $order];
Cache::put($cacheKey, $response, now()->addHour());
return response()->json($response, 201);
}
}Remarques : utilise un store partagé (Redis) pour Cache en production si tu as plusieurs instances, et pense à la rétention / purge des clés traitées. Laravel 13 n'introduit pas de middleware d'idempotence natif — tu dois donc l'implémenter toi-même ou passer par un package.
Package recommandé : square1-io/laravel-idempotency
Si tu préfères ne pas réinventer la roue, le package square1-io/laravel-idempotency ajoute un middleware d'idempotence robuste à ton API Laravel. Il gère la concurrence, la rétention des clés et les réponses mises en cache. Idéal pour un gain de temps sur un projet existant.
composer require square1/laravel-idempotencyUne fois installé, tu appliques le middleware à tes routes POST :
Route::post('/orders', [OrderController::class, 'store'])
->middleware('idempotency');Exemple Vue 3 + Axios (frontend)
En Vue 3, la Composition API et l'approche par composable sont la norme. Voici un exemple avec script setup :
// services/api.js
import axios from 'axios'
export const api = axios.create({
baseURL: import.meta.env.VITE_API_URL,
headers: { 'Content-Type': 'application/json' },
})
export function generateIdempotencyKey() {
return crypto.randomUUID()
}<script setup>
import { ref } from 'vue'
import { api, generateIdempotencyKey } from '@/services/api'
const loading = ref(false)
const productId = ref(1)
const quantity = ref(1)
const submitOrder = async () => {
loading.value = true
const idempotencyKey = generateIdempotencyKey()
try {
const response = await api.post('/orders', {
product_id: productId.value,
quantity: quantity.value
}, {
headers: { 'X-Idempotency-Key': idempotencyKey }
})
console.log(response.data)
} catch (err) {
console.error(err)
} finally {
loading.value = false
}
}
</script>Tu peux aussi fournir l'instance Axios globalement via app.provide ou un plugin, et l'injecter dans les composants selon les recommandations Vue 3.
Points avancés à considérer en production
• Concurrence : deux requêtes simultanées avec la même clé d'idempotence. Solutions : verrou pessimiste (Cache::lock dans Laravel), enregistrement d'un état "processing", ou middleware spécialisé comme celui du package square1-io.
• Persistance des résultats : choisir la durée de rétention en fonction du besoin métier (quelques heures pour un paiement, plusieurs jours pour une création de compte).
• Stripe comme référence : Stripe utilise Idempotency-Key (sans le préfixe X-) et conserve les clés pendant 24h minimum. C'est la référence à suivre.
• API Gateway : si tu utilises un API Gateway (Kong, APISIX, AWS API Gateway), il peut gérer l'idempotence au niveau proxy avant même que la requête n'atteigne ton backend Laravel. C'est une tendance 2026 pour les architectures microservices.