REST est mort sur mes SaaS. Voici ce qui l'a remplacé.
J'ai supprimé tous les endpoints REST de 3 de mes SaaS. Le résultat : sub-milliseconde end-to-end, zéro ligne de code pour le cache client, zéro invalidation manuelle. Voici ActualLab Fusion, et pourquoi c'est la techno qui a le plus transformé ma productivité ces 3 dernières années.
Le rituel REST que je faisais 100 fois
Tu connais le rituel. Je l'ai fait 100 fois. Tu l'as fait 100 fois.
// 1. Définir la route serveur
[HttpGet("/api/players/{id}")]
public async Task<PlayerDto> Get(string id) =>
_mapper.Map<PlayerDto>(await _db.Players.FirstAsync(p => p.Id == id));
// 2. Définir le DTO partagé (probablement dupliqué côté client)
export interface PlayerDto { id: string; name: string; level: number; }
// 3. Écrire le client API
const player = await fetch(`/api/players/${id}`).then(r => r.json());
// 4. Stocker dans un état React
const [player, setPlayer] = useState<PlayerDto>();
useEffect(() => { fetchPlayer(id).then(setPlayer); }, [id]);
// 5. Invalider quand quelque chose change (et là, l'enfer commence)
// - On vient de faire un POST /api/players/{id}/level-up
// - Il faut re-fetch le player
// - Mais aussi le scoreboard où le player apparaît
// - Et la liste des achievements liés
// - Et le profile du player ailleurs dans l'app
// - …
Six étapes par fonctionnalité. Dans le cas où rien ne change.
Quand quelque chose change, tu dois écrire toi-même ce qui doit être re-fetché. Et chaque oubli est un bug "l'écran de Marc n'est pas à jour".
Sur OneRP, ma plateforme multi-joueurs, il y a 48 écrans qui dépendent des données joueur. Si je gère ça à la main, j'ai 48 listes d'invalidation à maintenir. Manuellement. Pour chaque action. C'est de l'enfer.
C'est exactement le problème qu'Andrey Akinshin résout avec ActualLab Fusion.
Le concept en 30 secondes
Fusion est une bibliothèque .NET qui transforme n'importe quelle méthode serveur en abonnement réactif côté client.
Tu déclares une méthode :
public class PlayerService : IComputeService
{
[ComputeMethod]
public virtual async Task<Player> GetPlayer(string id, CancellationToken ct)
=> await _db.Players.FirstAsync(p => p.Id == id, ct);
}
Côté client (Blazor, JavaScript, n'importe quoi qui parle WebSocket binaire), tu t'abonnes :
const player = await fusion.compute('PlayerService.GetPlayer', { id: 'P-001' });
Et c'est tout. Pas de fetch. Pas d'invalidation. Pas de cache à écrire. Le framework :
- Cache automatiquement la réponse.
- Établit une dépendance entre ce client et cette méthode pour cet ID.
- Quand la donnée change, push automatiquement la nouvelle valeur via WebSocket.
Si Marc met à jour le niveau du player P-001, tous les 48 écrans qui affichent ce player se mettent à jour instantanément. Sans une ligne de code d'invalidation. Sans un event manuel. Le framework sait.
Comment Fusion sait ce qui dépend de quoi
C'est la magie qui m'a pris une semaine à comprendre.
Quand tu décores une méthode [ComputeMethod], Fusion la wrap dans un proxy.
Ce proxy intercepte tous les appels à d'autres [ComputeMethod] à l'intérieur
de la méthode et construit un arbre de dépendances.
Exemple :
[ComputeMethod]
public virtual async Task<PlayerScore> GetScore(string id, CancellationToken ct)
{
var player = await GetPlayer(id, ct); // dépendance #1
var weapons = await _weapons.GetForPlayer(id, ct); // dépendance #2
var multiplier = await _events.GetMultiplier(ct); // dépendance #3
return new PlayerScore { … };
}
Fusion enregistre : "GetScore('P-001') dépend de GetPlayer('P-001'),
GetForPlayer('P-001'), et GetMultiplier()."
Si n'importe laquelle de ces 3 méthodes invalide son résultat (par exemple
via Computed.Invalidate()), GetScore('P-001') est elle-même invalidée.
Et tous les clients abonnés sont notifiés.
Tu n'as rien à faire. Tu écris ton code métier normalement. Le système de dépendances est construit à l'exécution par le proxy.
L'autre moitié : CommandHandlers
Pour les écritures (mutations), Fusion utilise un pattern CQRS :
public class LevelUpCommand : ICommand<Unit>
{
public required string PlayerId { get; init; }
public required int Levels { get; init; }
}
public class PlayerCommandHandler : ICommandHandler<LevelUpCommand>
{
[CommandHandler]
public virtual async Task<Unit> Handle(LevelUpCommand cmd, CommandContext ctx, CancellationToken ct)
{
var player = await _db.Players.FirstAsync(p => p.Id == cmd.PlayerId, ct);
player.Level += cmd.Levels;
await _db.SaveChangesAsync(ct);
using var _ = Computed.Invalidate();
_playerService.GetPlayer(cmd.PlayerId, default).Ignore();
// ← cette ligne dit "invalide GetPlayer pour ce player".
// Fusion s'occupe du reste.
return default;
}
}
Tu déclenches LevelUpCommand, le handler modifie la DB, et dit à Fusion ce
qui change. Tous les calculs (et donc les écrans) qui dépendent de cette
donnée sont mis à jour.
C'est plus de code qu'un POST /api/players/X/level-up. Mais c'est aussi
explicite, traçable, testable. Une commande, un handler, une invalidation.
Pas de "il faut aussi mettre à jour le scoreboard, et le profile, et…"
Les vrais chiffres sur OneRP
| REST classique | Fusion | |
|---|---|---|
| Endpoints à maintenir | 200+ | 0 |
| ComputeServices | — | 111 |
| CommandHandlers | — | 24 |
| DTO à dupliquer | 200+ | 0 (génération auto) |
| Lignes de cache client | ~2 000 | 0 |
| Lignes d'invalidation | ~3 500 | ~200 (dans les handlers) |
| Latence p99 invalidation | 200-800 ms (polling) | 0.8 ms |
| Updates par seconde | 200/s (limite navigateur) | 14 200/s |
Le chiffre qui me marque le plus : 0 ligne de cache client. Pas une seule. Toute la complexité du cache reactive est abstraite.
Le piège : la courbe d'apprentissage
Je vais être honnête. Fusion a une courbe d'apprentissage brutale.
- Le concept de "ComputeMethod doit être virtual" est étrange au début.
- La distinction entre
Computed<T>etTask<T>prend un moment. - Les
CommandContextetusing Computed.Invalidate()ont l'air magiques avant de comprendre. - La doc est dense (Andrey est russe, il écrit en anglais technique de très haut niveau).
J'ai mis 2 semaines à temps plein à comprendre Fusion à fond. Sur 10 développeurs que j'ai vu essayer, 8 abandonnent au mois 1 parce qu'ils ne dépassent pas la phase "ça compile pas, je sais pas pourquoi".
Une fois passé ce cap, on ne veut plus revenir en arrière. J'ai 3 SaaS sur Fusion. Je n'ai pas écrit une ligne de REST sérieuse depuis 18 mois.
Quand utiliser Fusion vs REST classique
Fusion brille quand :
- ✅ Multi-écran temps-réel (dashboards, jeux, collab)
- ✅ Mutation cascade complexe (un change → 10 écrans bougent)
- ✅ Stack 100 % .NET (backend + Blazor + .NET client mobile)
- ✅ Polling fréquent évité (objectif : push live)
REST reste mieux pour :
- ❌ API publique consommée par des partenaires tiers (Fusion est binaire, WebSocket, pas auto-documenté)
- ❌ Intégrations webhooks (le pattern n'est pas adapté)
- ❌ Stack hétérogène (Fusion .NET → Python = non trivial)
- ❌ Petites apps où l'overhead conceptuel n'est pas rentable
Sur OneRP, SaleCast, PromptVault, Racine : Fusion partout, zéro REST entre domaines internes. Sur les intégrations marketplaces de SaleCast (PrestaShop, Shopify, Amazon…) : REST classique avec connecteurs.
C'est exactement le bon mix.
La leçon
REST a 25 ans. Ça reste utile pour les API publiques. Mais pour le front-end de mes SaaS, c'est une abstraction qui m'a coûté du temps pendant 10 ans.
Le passage à Fusion m'a appris une chose plus large :
Quand une abstraction te fait écrire le même code 100 fois, c'est qu'elle n'est pas la bonne abstraction.
REST te fait écrire 100 fois "fetch → setState → invalidate manually". Fusion te fait écrire 0 fois ça. C'est cette différence qu'il faut chercher dans toute nouvelle techno.
Pas "est-ce que c'est plus rapide ?" (REST + cache bien fait est rapide). Pas "est-ce que c'est plus moderne ?" (subjectif).
"Est-ce que ça supprime un type entier de code que j'écrivais sans questionner ?"
Si oui, prends-toi 2 semaines pour apprendre. Le ROI sur 3 ans est colossal.
Si non, reste sur l'existant.
Stack & code
- ActualLab Fusion 12.x (.NET 10)
- 111 ComputeServices sur OneRP, 51 sur SaleCast, 23 sur PromptVault
- MessagePack + MemoryPack pour la sérialisation binaire (4-6× plus rapide que JSON)
- WebSocket pour le transport (le polling longue durée est une option mais on l'évite)
- Blazor Server côté client web (interop natif Fusion) + clients React via des bindings TypeScript générés
- 3 SaaS en production sur cette stack, zéro problème de scale sur 2 048 connexions concurrentes
Si tu fais du .NET et que tu n'as jamais entendu parler de Fusion, prends 3 heures ce week-end pour lire la doc. C'est l'investissement avec le meilleur ROI que tu peux faire sur ta stack en 2026.
Florian Sola
Lead Technique · Haute performance temps réel · 9 ans d'expérience