Fil rouge : une API Node/TypeScript type Medusa avec middleware d'authentification, un front React qui charge des données avec react-query, et un stockage de mots de passe et de tokens.
Sémantique HTTP
QQue t'apprend la classe du code (4xx vs 5xx) sur l'endroit de la faute ?›
4xx = faute côté client : requête mal formée, non authentifiée, ressource inexistante — corriger la requête. 5xx = faute côté serveur ; la réponse est volontairement muette (« unknown error ») pour ne pas exposer l'interne — aller lire les logs.
« Le code dit qui doit agir, donc un 4xx renvoie le client à sa requête et un 5xx renvoie le développeur à ses logs. »
Q401 vs 403 ?›
401 = je ne sais pas qui tu es (authentification) : réessayer avec des identifiants. 403 = je sais qui tu es et tu n'as pas le droit (autorisation) : réessayer ne sert à rien. Le nom « 401 Unauthorized » est historiquement mal choisi.
« L'authentification établit l'identité et l'autorisation vérifie les droits, donc 401 manque l'identité et 403 manque le droit. »
QUn client envoie DELETE, la réponse se perd, il réessaie. Que répondre ?›
Un succès, pas un 404. DELETE signifie « fais que cette ressource n'existe plus » : l'état visé est atteint. C'est l'idempotence : GET, PUT, DELETE le sont ; POST ne l'est pas — d'où les clés d'idempotence pour les paiements.
« Une requête idempotente décrit un état cible, donc la rejouer ne change rien, donc l'état atteint est un succès même si la ressource a déjà disparu. »
QStripe et Medusa mettent à jour avec POST /resource/:id, jamais PATCH. Quel raisonnement ?›
REST n'est pas une norme mais un style : aucune autorité ne valide la conformité, chaque plateforme arbitre. Ce qu'on gagne : un seul verbe, moins de surface, pas de débat PUT/PATCH. Ce qu'on perd : POST signifie création ou mise à jour selon que le chemin porte un id, et POST n'est pas idempotent par convention.
« Le style REST ne contraint pas les verbes, donc une plateforme peut choisir la simplicité, donc POST partout, au prix d'une sémantique qui se lit dans le chemin. »
QLe middleware s'exécute-t-il avant ou après le routing ? Conséquence observable ?›
Avant. Il peut interrompre la requête avant que le serveur sache si la route existe : une URL inexistante sans l'en-tête exigé renvoie l'erreur du middleware (400/401), pas 404.
« Le middleware est traversé par toute requête avant la résolution de route, donc son verdict précède le 404, donc une route absente peut répondre 401. »
Runtime JS / TS — ce qui n'existe pas à l'exécution
QPromise { <pending> } ou [object Promise] dans un log : que signale-t-il ?›
Un await oublié. Une fonction async retourne toujours une promesse ; sans await on manipule la promesse, pas sa valeur. Le symptôme apparaît à la première concaténation ou sérialisation.
« async enveloppe toute valeur de retour dans une promesse, donc sans await on obtient l'enveloppe, donc le log affiche un objet Promise. »
Q?? vs || : quand basculer sur l'opérande de droite ?›
?? bascule seulement si la gauche vaut null ou undefined ; || bascule sur toute valeur falsy (0, '', false). Pour un remplaçant sur un champ nullable, ?? : une chaîne vide ou un 0 restent des valeurs.
« 0 et la chaîne vide sont des valeurs légitimes, donc un || les écraserait, donc on utilise ?? qui ne teste que l'absence. »
Qconst xs = [] sans annotation en TypeScript : quel type, quelle conséquence ?›
never[] — valide tant qu'on ne lit rien dedans : never est le type sans aucune valeur, on ne peut lire aucune propriété. Annoter const xs: string[] = [].
« Un tableau vide n'a aucun élément pour inférer un type, donc TS choisit never, donc toute lecture est une erreur de type. »
QUn endpoint typé reçoit {} alors que le type déclare trois champs obligatoires. Pourquoi rien ne l'arrête ?›
Les types TypeScript sont entièrement effacés à la compilation : il n'en reste rien à l'exécution. Un type décrit un contrat entre développeurs ; un schéma runtime (zod) le fait respecter face à un client. Les deux doivent rester alignés (inférer le type depuis le schéma).
« Le type n'existe que pour le compilateur, donc un client externe ne le voit jamais, donc il faut valider les données à l'entrée avec un schéma exécuté. »
QUn import npm qui fonctionne aujourd'hui peut cesser de résoudre demain sans toucher au code. Pourquoi ?›
Le paquet n'est pas déclaré dans package.json : il n'est présent que parce que npm l'a installé comme dépendance transitive d'un autre. Une mise à jour de cet autre peut le retirer. Déclarer tout import direct transforme « ça marche par hasard » en contrat.
« Une dépendance transitive appartient à quelqu'un d'autre, donc elle peut disparaître à sa prochaine version, donc tout ce qu'on importe doit être déclaré chez soi. »
React et fetch
QComment un composant React affiche-t-il une donnée asynchrone alors que sa fonction est synchrone ?›
Il n'attend pas. Au premier appel il s'exécute entièrement avec la donnée absente, retourne un état de chargement ; quand la donnée arrive, un changement d'état déclenche un nouveau rendu. C'est pourquoi un composant ne peut pas être async : React a besoin d'un retour immédiat.
« Le rendu doit produire un arbre tout de suite, donc on rend l'absence, donc l'arrivée de la donnée provoque un second rendu. »
QDeux useQuery de même queryKey dans le même rendu : combien de requêtes, et que vaut isPending ?›
Une seule requête, et isPending vaut quand même true pour les deux. Le second trouve l'entrée dans le cache, mais l'entrée est encore vide : trouver l'entrée n'est pas trouver la donnée. C'est de la déduplication, pas un hit de cache.
« Le cache est indexé par clé dès la première demande, donc la seconde s'y attache, donc une seule requête part, mais la donnée n'est pas encore là, donc les deux attendent. »
QDans quels cas la promesse de fetch() est-elle rejetée ?›
Uniquement sur un échec de transport : DNS, connexion coupée, blocage CORS. Toute réponse HTTP, 404 ou 500 comprise, est une résolution. Il faut tester res.ok et lever soi-même, sinon res.json() parse un corps d'erreur.
« fetch réussit dès qu'une réponse arrive, donc un 500 est un succès pour lui, donc l'erreur métier se lit dans res.ok, pas dans le catch. »
QValeur par défaut de credentials dans fetch, et que décide-t-elle ?›
same-origin : les cookies partent seulement vers la même origine (schéma + hôte + port). Vers une autre origine il faut credentials: 'include' et un serveur qui l'autorise (CORS). Le cookie appartient au navigateur ; la requête décide seulement s'il l'accompagne.
« Une origine est le triplet schéma-hôte-port, donc un port différent est une autre origine, donc les cookies ne partent pas sans include. »
QUne route renvoie à l'utilisateur ses enregistrements : lecture puis filtre en mémoire. Quel bug silencieux ?›
Si l'ORM applique une taille de page par défaut, la limite s'applique à la population entière, avant le filtre : l'utilisateur ne voit que ses lignes tombées dans la première page. C'est un 200 avec un JSON bien formé, aucune erreur, aucun log — et ça dépend de la volumétrie. Filtrer dans la requête (WHERE owner_id = ?).
« La pagination coupe avant que le filtre applicatif ne voie les lignes, donc des enregistrements légitimes disparaissent sans erreur, donc le filtre doit être dans la requête. »
Secrets et autorisation
Qbcrypt et argon2 : en quoi diffèrent-ils de SHA-256 ?›
Fonctions de hachage de mots de passe : délibérément lentes, coût réglable (facteur de travail), sel intégré. SHA-256 est conçu rapide : des milliards d'essais par seconde sur GPU. bcrypt.hash(pwd) renvoie une chaîne contenant coût, sel et digest.
« Un mot de passe est devinable, donc la défense est de rendre chaque essai coûteux, donc on choisit une fonction lente à coût réglable. »
QQu'apporte exactement un sel, et qu'est-ce qu'il n'apporte pas ?›
Il désolidarise les lignes : deux utilisateurs de même mot de passe ont des digests différents, donc pas de table précalculée ni d'attaque « une pour tous ». Sans sel, 10⁹ hachages couvrent 10⁷ utilisateurs d'un coup. Il ne rend pas un mot de passe faible fort : un attaquant qui vise une ligne n'est pas ralenti.
« Le sel rend chaque digest unique, donc l'attaquant doit recommencer par ligne, donc il perd le parallélisme, mais la force par ligne vient du coût, pas du sel. »
QToken porteur opaque de 32 octets aléatoires : pourquoi le hacher en SHA-256 et non bcrypt ?›
bcrypt défend des secrets devinables, à faible entropie ; 32 octets aléatoires ont 256 bits d'entropie, indevinables. Décisif : le token est la clé de recherche — on doit retrouver la ligne par le hash, donc le hash doit être déterministe sans sel ; bcrypt, salé, oblige à tester chaque ligne. Avec un mot de passe, l'e-mail désigne la ligne, puis on compare.
« Un secret à haute entropie n'a pas besoin d'être ralenti, donc SHA-256 suffit, et le hash doit servir d'index, donc il doit être déterministe. »
QUn secret cryptographique en Node : d'où le tirer, et pourquoi pas Math.random ?›
crypto.randomBytes : un secret ne vaut jamais plus que sa source d'aléa. Math.random couvre au mieux 2⁵³ états, souvent moins, et son état interne est reconstructible depuis quelques sorties.
« Un générateur prévisible rend le secret prévisible, donc il faut une source cryptographique, donc crypto.randomBytes. »
QUne ressource existe mais appartient à quelqu'un d'autre : 403 ou 404 ?›
404. Un 403 confirme l'existence : en itérant sur des identifiants, l'appelant énumère ce qui existe. Quand la portée d'accès est dans la requête (WHERE owner_id = ?), le 404 n'est pas un mensonge : pour cet utilisateur, la ressource n'existe pas.
« Répondre 403 révèle qu'il y a quelque chose à protéger, donc on ne distingue pas absence et interdiction, donc 404 dans les deux cas. »