Réseau et cache en entreprise : assumez les données périmées
Réseau et cache en frontend d'entreprise : une frontière , des contrats d'API typés, annulation, dédoublonnage, Cache-Control, relances sûres.

Un client effectue un virement, revient au tableau de bord et voit l'ancien solde. Le virement est bien passé. Le tableau de bord a lu un cache que personne n'avait prévenu de l'écriture. Dans une banque, cela donne un appel au service client, et parfois un deuxième virement.
Le cache figure dans cette partie, à côté du réseau, parce que dans une application d'entreprise les deux ne forment qu'une question : où vit cette donnée, et quand est-elle fausse ? Un cache sans propriétaire est un bug à retardement.
Une seule frontière vers le backend
Les composants ne font jamais d'appels réseau. Une couche data-access par domaine parle au backend, et tout le reste lit depuis elle. Vous avez ainsi un seul endroit pour ajouter l'authentification, un seul pour annuler, un seul pour invalider.
Quand les écrans ont besoin d'une forme de données que les API du domaine n'offrent pas, placez entre les deux un (BFF), qui appartient à l'équipe frontend. Faites-le pour la forme des données et pour les secrets, pas pour éviter de parler à l'équipe API.
Le contrat est généré, et required fait foi
Générez les types à partir du document au lieu de les écrire à la main. Les schémas OpenAPI 3.1 sont « un sur-ensemble de la spécification JSON Schema Draft 2020-12 », et en , « les propriétés définies par le mot-clé properties ne sont pas obligatoires » tant qu'une liste required ne les nomme pas. Pour savoir ce qui sera présent, lisez donc le tableau required, pas la liste des champs.
Annuler, dédoublonner, limiter dans le temps
Chaque requête a besoin de trois choses. Annulez ce que l'utilisateur a quitté : le httpResource d' « annule la requête en cours avant d'en émettre une nouvelle » quand ses entrées changent. Sans cela, la documentation de React décrit le résultat : une , où une réponse plus ancienne arrive en dernier et l'écran affiche les mauvais résultats. Avec un simple fetch, AbortController permet d'« annuler une ou plusieurs requêtes web au moment voulu ».
Dédoublonnez ce qui est déjà en cours, pour que cinq widgets qui demandent les comptes ne déclenchent qu'une seule requête. Et donnez un timeout à chaque appel, pour qu'un backend lent se traduise par un message d'erreur, pas par une roue de chargement qui tourne sans fin.
Après une écriture, qui rafraîchit le cache ?
Invalider une entrée de cache, c'est la marquer comme dépassée, pour que la lecture suivante retourne au serveur. Chaque cache a besoin d'un propriétaire, et ce propriétaire, c'est le code qui effectue l'écriture. Après un virement, le des comptes recharge ses données ou applique la réponse du serveur. Un widget qui garde sa propre copie n'en saura jamais rien.
Avec React sans framework, la couche de données est à vous de la choisir ou de la construire. Sa documentation cite le cache, la récupération côté serveur et les cascades réseau comme les parties difficiles, et oriente vers la récupération de données intégrée aux frameworks plutôt que vers les effects. Quelle que soit la bibliothèque, la décision reste la même : quelles clés cette écriture rend-elle fausses ?
Le code qui écrit la donnée est propriétaire de son cache.
Sur mobile au Maroc, le cache d'abord
Sur mobile au Maroc, le cache n'est pas une optimisation. En dehors des grandes villes, la connexion est souvent lente ou absente, et un écran qui attend le réseau est un écran que l'on ferme. Dans Qismi, notre application scolaire, nous avons réglé cela en misant fortement sur le cache :
- Deux niveaux. Chaque liste est gardée en mémoire et dans le stockage du téléphone, chaque entrée avec une durée de vie () : cinq minutes pour les fils de messages, une heure pour les annonces.
- Le cache d'abord, le rafraîchissement derrière. L'écran affiche tout de suite la copie en cache et en récupère une nouvelle en arrière-plan. Ce pattern s'appelle .
- Le réseau décide du moment. En Wi-Fi, une liste en cache est considérée comme périmée après 20 % de son TTL, en 3G après 80 %, en 2G après 95 %, et hors ligne elle n'est jamais rafraîchie. Les connexions lentes gagnent en rapidité, les rapides en fraîcheur.
- L'écriture reste propriétaire du cache. Après l'envoi d'un message, le code qui l'a envoyé vide quatre entrées : les listes de fils des deux personnes, la conversation, et le compteur de non-lus du destinataire.
Image : tt-tech.io, adapté de web.dev, Keeping things fresh with stale-while-revalidate, CC BY 4.0.
L'image montre le même pattern pour une réponse HTTP avec un max-age d'une seconde et une fenêtre stale-while-revalidate de 59 secondes.
C'est la même règle que plus haut, simplement avec davantage de caches dont il faut être propriétaire.
Les en-têtes de cache HTTP
Le est le deuxième cache et, dans la banque, celui qui fuit. no-store signifie que « aucun cache, de quelque type que ce soit (privé ou partagé), ne doit stocker cette réponse » ; private sert pour le « contenu personnalisé pour l'utilisateur ». Un contenu personnalisé dans un cache partagé « peut provoquer une fuite d'informations involontaire ». Les assets de hachés sont le cas inverse : un long max-age avec immutable.
Image : tt-tech.io, adapté de web.dev, Prevent unnecessary network requests with the HTTP Cache, CC BY 4.0.
Relances et idempotence
Relancer un GET est sans risque. Relancer un virement ne l'est pas. POST et PATCH « ne sont pas garantis idempotents », et un client peut relancer sans risque une requête « qui utilise une méthode ». Une écriture porte donc une clé que le serveur dédoublonne, ou bien elle n'est jamais relancée automatiquement. Quand le serveur répond 409 Conflict, soit « un conflit entre la requête et l'état actuel de la ressource cible », montrez-le à l'utilisateur et rechargez ; ne relancez pas.
C'est dans les que vivent les aspects transversaux : en-têtes d'authentification, relances, timeouts, logs. Limitez les relances aux lectures. J'y ajouterais aussi un identifiant de corrélation, pour qu'un journal d'audit côté serveur puisse être rapproché de ce sur quoi l'utilisateur a cliqué.
Sur Atlas Bank
Atlas n'a pas de vrai serveur. FakeBackend, dans libs/data-access/banking, est la frontière (son commentaire dit que « rien d'autre dans l'app ne parle au “serveur” »), avec 250 ms de latence simulée pour que les états de chargement soient réels. AccountsStore.load() dédoublonne :
/** Idempotent : les appels simultanés partagent une seule requête en cours ; les appels suivants se résolvent immédiatement. */
load(): Promise<void> {
if (this.loadedState()) return Promise.resolve();
this.inFlight ??= this.fetchAll();
return this.inFlight;
}Après un virement, le même store ajoute l'opération renvoyée par le backend et soustrait le montant du solde, si bien que chaque écran qui le lit est juste. Le échoue sur un fetch vers une URL absolue, parce que « le navigateur n'appelle jamais directement un tiers ». Ce qu'Atlas n'a pas encore : l'annulation, les timeouts, et une sur transfer(). Tout cela viendra avec une vraie couche HTTP.
Ce que j'ai laissé de côté, et pourquoi
- contre . En entreprise, c'est généralement le backend qui décide.
- , , en tour d'horizon. Quand vous avez besoin du temps réel, c'est une seule décision, pour un seul flux.
- et sur le web. Rares dans les applications web réglementées. Sur mobile, c'est une autre histoire, comme le montre Qismi.
- Le cache en périphérie des . De l'exploitation, pas de la conception frontend.
Dès lundi
- Cherchez
fetch(etHttpClientdans votre application en dehors de la couche data-access. Chaque résultat est une frontière manquante. - Prenez votre écriture la plus importante. Notez quelles données en cache elle rend fausses, et qui les rafraîchit.
- Vérifiez l'en-tête
Cache-Controlde votre réponse la plus sensible.
Ensuite : la partie 4, sur le rendu.
Pour votre agent de code
Les règles de cet article, écrites pour un agent. Collez-les dans votre AGENTS.md, enregistrez-les comme , ou indiquez le fichier à votre agent.
# Networking and caching rules for enterprise apps Source: https://tt-tech.io/blog/enterprise-frontend-networking-caching For Angular and React frontends. Add to AGENTS.md or save as a skill. Your project's own rules win where they differ. ## Rules 1. Components never fetch. All backend calls go through one data-access layer per domain. No `fetch(` or `HttpClient` outside it. 2. Add a backend for frontend (BFF), owned by the frontend team, only when the screens need a shape the domain APIs do not have, or to keep secrets out of the browser. 3. Generate API types from the OpenAPI document. Never write them by hand. Treat a field as always present only when the schema's `required` array names it. 4. Cancel requests the user left: `httpResource` in Angular, `AbortController` with `fetch`. An older response must never overwrite a newer one. 5. Dedupe requests already in flight, so several widgets asking for the same data cause one request. 6. Give every call a timeout, so a slow backend becomes an error message, not an endless spinner. 7. The code that performs a write owns the cache of that data. After the write, refetch or apply the server's answer in the owning store. Never keep a private copy in a widget. 8. Set `Cache-Control` on purpose: `no-store` for responses that must not be reused (an account balance), `private` for user-personalised content, never personalised content in a shared cache, and a long `max-age` plus `immutable` for hashed build assets. 9. Retry reads only. Never retry a `POST` or `PATCH` automatically unless it carries an idempotency key that the server deduplicates. 10. On `409 Conflict`, show the conflict to the user and reload. Do not retry. 11. Put cross-cutting concerns in interceptors: auth headers, timeouts, read retries, logging, and a correlation id per request so server logs can be matched to what the user clicked. 12. On mobile, where the connection is often slow or gone, cache first: show the cached copy at once and refresh it in the background (stale-while-revalidate), with a time to live per entry. The write still clears what it makes wrong. ## Before you say done - Search the diff for `fetch(` and `HttpClient` outside the data-access layer. Move each hit behind the boundary. - For every write in the diff, list in the pull request which cached data it makes wrong and where it is refreshed. - Check every retry in the diff: it is on a read, or the write carries an idempotency key. - For every new or changed sensitive response, state its `Cache-Control` header.
Sources
- OpenAPI Initiative, OpenAPI Specification v3.1.1, 2024
- JSON Schema, object, documentation
- Angular, Reactive data fetching with httpResource, documentation
- React, You Might Not Need an Effect, documentation
- MDN, AbortController
- MDN, Cache-Control header
- MDN, HTTP caching
- MDN, Idempotent
- MDN, 409 Conflict
- Angular, Intercepting requests and responses, documentation
- web.dev, Keeping things fresh with stale-while-revalidate
- web.dev, Prevent unnecessary network requests with the HTTP Cache