Skip to content

feat(server): socle API-first — routes JSON, validation, logs structurés et squelettes - #53

Merged
mickael-coquer-igocreate merged 80 commits into
masterfrom
feat/api-first-phase1
Sep 17, 2026
Merged

mickael-coquer-igocreate merged 80 commits into
masterfrom
feat/api-first-phase1

Conversation

@mickael-coquer-igocreate

@mickael-coquer-igocreate mickael-coquer-igocreate commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Phase 1 de la feuille de route : de quoi démarrer un projet igo en back et React en front, et le voir tourner en production.

Additif pour les projets existants, à deux exceptions signalées en bas.


Le framework — @igojs/server

Routes JSON

app.api('/books', routes) monte sous config.api.prefix. Ces routes répondent en JSON quoi qu'il arrive — 500, 404, JSON malformé, validation — au format RFC 9457. Erreurs identifiables par programme (type sur le document, code par champ), jamais par leur libellé.

Validation sans boilerplate

exports.create.body = dto.CreateBook;   // req.body arrive validé et coercé

Tout schéma Standard Schema, pas seulement zod.

Types depuis le schéma

ApiHandler<{ body: typeof dto.CreateBook }> type req.body sans redéclarer la forme. igo reste du JavaScript : ce sont des .d.ts.

Santé

GET /health (le process vit) et GET /health/ready (base, cache, disque). La readiness rend 503 dès qu'une dépendance critique manque — ce que lit un répartiteur de charge. Une sonde 'optional' est signalée sans faire échouer : le cache l'est par défaut, igo servant sans lui.

Observabilité

Logs structurés avec identifiant de trace, traceresponse sur chaque réponse, LOG_REQUESTS avec plancher de statut, rédaction des champs sensibles.

En-têtes de sécurité

config.security sur chaque réponse, valeurs issues d'un pentest. Pas de dépendance ajoutée.


Le squelette fullstack

igo create --skel=fullstack : monorepo pnpm, API TypeScript + SPA React, oxlint et oxfmt, tests unitaires et E2E, CI GitHub.

Observabilité de bout en bout

deploy/config.alloy.agent.example un par machine — logs, système, OTLP local
deploy/config.alloy.gateway.example un pour l'infrastructure — bases scrutées à distance, sonde de disponibilité
deploy/grafana-dashboard.example.json 35 panneaux, quatre signaux dorés en tête
deploy/grafana-alertes.example.json 14 règles, 2 désactivées par défaut

C'est la passerelle qui absorbe la variabilité des hébergeurs : le tableau de bord interroge les mêmes métriques partout, elle va les chercher là où elles sont. OVH et Scaleway documentés.

Front

Faro pour erreurs, Web Vitals et corrélation avec les traces serveur. Session en mémoire (persistent: false) : rien n'est écrit dans le navigateur, pas de consentement requis.

Déploiement

nginx.conf.example, en-têtes de sécurité, CSP de la SPA en <meta>.


À regarder en priorité

  1. connect/health.js — deux routes, la distinction critique/facultatif.
  2. api/validate.js et api/problem.js — le cœur du socle API.
  3. grafana-alertes.example.json — les seuils sont des choix, à contester.
  4. config.alloy.gateway.example — c'est là que se règle le passage à OVH ou Scaleway.

Ce qui change pour un projet existant

  • Une erreur sous /api rend du JSON au lieu d'une page HTML 500.
  • Les logs de production passent en JSON. config.logformat = 'human' restaure l'ancien format.

Vérifications

797 tests sur les quatre paquets. Le squelette généré à blanc, installé, exécuté :

format lint types test build e2e
api ✓ ✓ ✓ 7 ✓ —
front ✓ ✓ ✓ 6 ✓ —
e2e ✓ ✓ ✓ — — 3

Le tableau de bord et les alertes ont été construits contre des données réelles, pas déduits des noms attendus — la pile pousse des métriques OTel (http_server_request_duration), et c'est cette supposition qui avait coûté un tableau de bord entier.

Trois pannes provoquées (MySQL arrêté, API arrêtée, Redis arrêté) ont validé la chaîne complète jusqu'à l'email, et révélé quatre défauts qu'aucune relecture n'aurait montrés :

  • up mesure le collecteur, pas le service : pendant une panne MySQL il restait à 1 ;
  • un for: 5m plus long que la panne n'arme jamais l'alerte ;
  • une comparaison PromQL conserve la valeur d'origine, donc probe_success < 1 rendait zéro — invisible pour une condition « supérieur à zéro » ;
  • une application morte n'émet plus rien : seule une sonde extérieure constate son absence.

Les deux commits marqués ! ne touchent que le squelette : aucune API publiée ne change.

🤖 Generated with Claude Code

- Les aliases @controllers/, @services/ ne viennent pas d'igo mais de
  module-alias déclaré par le projet : l'ADR les présentait à tort comme
  imposés par le framework.
- Format d'erreur API : RFC 9457 Problem Details, imposé plutôt que laissé
  au choix de chaque projet.
- Validation : middleware global monté par igo, schéma attaché au handler,
  signature en Standard Schema.
- Squelette greenfield sans alias — les fichiers d'une feature sont côte à côte.
- Feuille de route : observabilité réalignée sur Grafana Cloud (l'ADR fusionné
  a écarté Sentry), .d.ts et squelettes remontés en phase 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Les routes montées via app.api() vivent sous config.api.prefix ('/api' par
défaut) et répondent en JSON quoi qu'il arrive — 500, 404, JSON malformé et
erreurs de validation. Un front React ne reçoit plus de page HTML.

La validation est globale : le schéma est attaché au handler
(controller.create.body = dto.CreateBook), igo enveloppe les handlers au
démarrage. Rien à écrire dans les routes. Les routes à corps sans schéma sont
signalées au boot, sans jamais bloquer le démarrage.

Express 5 impose deux détours, tous deux vérifiés :
- req.query est un getter : une affectation directe échoue en silence, d'où
  Object.defineProperty pour propager la valeur coercée.
- le chemin de montage d'un routeur n'est plus lisible, d'où app.api() qui
  donne le préfixe à igo par construction.

La signature accepte tout schéma Standard Schema (zod, valibot, arktype).

dev.agent expose res.data — res.json() sérialise via res.send(), sans quoi
chaque test devrait parser res.body lui-même.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Les schémas Zod deviennent la source des types : ApiHandler<{ body: typeof
CreateBook }> donne req.body typé, coercitions comprises, sans redéclarer la
forme. app.api() est ajouté à l'interface Express.Application.

Les projets JavaScript ne voient aucune différence — un .d.ts n'est jamais
chargé à l'exécution, et @standard-schema/spec est une dépendance de types.

test/types/expect-errors.ts épingle les erreurs qui doivent se déclencher :
si l'inférence retombe sur `any`, tsc signale les @ts-expect-error inutilisés
et typesTest échoue. Sans ça, une déclaration cassée passerait inaperçue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
igo create <projet> --skel=api|api-ts, à côté de tailwind resté par défaut.

Les deux livrent le même domaine d'exemple — modèle ORM, DTO, contrôleur,
routes, migration et tests d'intégration couvrant les quatre cas de l'ADR
(nominal, validation, 404, champs exposés). Ni dust, ni webpack, ni scss.

api-ts démontre que les schémas Zod suffisent à typer req.body et req.query :
aucune interface n'est maintenue en double. Il tourne sous tsx en dev et
compile vers dist/ ; le typecheck remplace eslint, qui ne sait pas lire du TS
sans typescript-eslint.

Les deux squelettes ont été générés, installés et exécutés contre une vraie
base : 7 tests passent de chaque côté, et le build TS sert l'API.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nouvelle page server/api : montage via app.api(), validation par schéma
attaché au handler, format RFC 9457, DTO, tests et typage TypeScript.
Liens croisés depuis routes, errors et getting-started, qui décrivaient
encore un serveur qui ne rend que du HTML.

Corrige au passage le build vitepress, cassé avant cette branche : un
Loaded<T> non échappé était lu comme une balise, et trois liens pointaient
vers des documents jamais commités.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le type était constamment about:blank et le code d'erreur par champ était
jeté : un client ne pouvait discriminer qu'en lisant title ou message, des
libellés d'affichage qui bougent avec la version et la locale de zod.

- Les erreurs de validation portent type: urn:igo:validation-failed.
- Chaque entrée de errors[] porte le code zod (invalid_type, too_small…),
  quand la bibliothèque en fournit un — ce n'est pas garanti par Standard
  Schema.
- Les titres viennent de http.STATUS_CODES au lieu d'une liste tenue à la
  main où 409, 429 et le reste tombaient sur « Error ».

Les types métier restent à la charge de l'application : igo n'en connaît
qu'un. La doc explique le mécanisme, propose /problems/<slug> comme
convention, et montre le contre-exemple qui manquait sur les DTO.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
/problems/<slug> est un URI relatif : il appartient au domaine de
l'application. Les erreurs du framework ne peuvent pas y vivre — le même
problème aurait un identifiant différent sur chaque projet, et empiéterait
sur les slugs de l'application. La doc proposait la convention sans dire
pourquoi igo ne la suit pas lui-même.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
wire() placé avant routes.init() ne voit aucune route et n'enveloppe rien :
la validation devient inactive sans erreur ni warning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TypeScript 7 est la version stable ; le monorepo était resté en 5. La montée
révèle deux ruptures que seul un squelette généré à blanc pouvait montrer :

- moduleResolution 'node' (node10) est supprimé — retiré du tsconfig.
- les globales ambiantes ne sont plus incluses d'office — types: [node, mocha]
  déclaré explicitement, sans quoi describe/it ne compilent pas.
- typescript/bin/tsc n'est plus résolvable (exports map) : typesTest passe par
  le manifeste du paquet, ce qui marche de TS 5 à 7. Au passage, son fallback
  this.skip() ne pouvait pas fonctionner dans une arrow function.

Les nouveaux projets démarrent sur Node 24 : engines sur les deux squelettes
API, @types/node 24, cible ES2023.

Vérifié à blanc : typecheck, build, 7 tests et l'inférence depuis les schémas.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mocha 12 est sorti fin août ; la plage ^11.0.0 le refusait, au point que
npm bloquait l'installation. Les 724 tests des quatre paquets passent en 12.

Trouvé en essayant pnpm sur un squelette généré : son mode strict signale les
peers non satisfaits, là où npm installe en silence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
process.exit(1) était inconditionnel : une exception survenue pendant une
requête déjà répondue tuait quand même le serveur. config.exitOnUncaughtException
= false permet de rester en vie dans ce cas précis.

Hors contexte de requête, on sort toujours, même option désactivée : Node ne
garantit rien sur l'état du process et rien ne peut en répondre.

Le défaut reste inchangé — l'option n'a de sens qu'une fois l'alerting
détaché du crash → mail.

Le script du test vit dans fixtures/*.cjs : sous test/**/*.js, mocha le
chargeait comme un fichier de test et il tuait le runner, tronquant la suite
sans message d'échec.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Les nouveaux projets partent en TypeScript ; la variante JavaScript n'aurait
pas eu de client. skel/api-ts devient skel/api, l'ancien skel/api disparaît.

Fait avant publication : après, retirer une valeur de --skel aurait été
cassant.

Les refontes ne passent pas par le squelette — leur projet existe déjà — mais
skel/api reste leur référence de structure. Un projet JS peut d'ailleurs
charger du .ts sans build via tsx, ce qui rend l'ajout d'une API TypeScript
à un back existant progressif.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le logger produisait des lignes de texte colorisées, y compris en production :
les codes ANSI polluaient la sortie et le second argument était jeté. Un
logger.info('ok', { user_id: 42 }) perdait silencieusement user_id, et la
stack d'une erreur n'apparaissait nulle part.

- JSON en production, lisible ailleurs (LOG_FORMAT pour forcer). Les
  métadonnées deviennent des champs, la stack et les codes SQL aussi.
- Une ligne par requête : méthode, chemin, statut, durée. Le niveau suit le
  statut (5xx error, 4xx warn).
- Un identifiant de requête, exposé par req.id, l'en-tête X-Request-Id, et
  estampillé sur chaque log de la requête sans rien passer en paramètre.
  C'est ce qui relie les lignes d'une requête entre elles, et un rapport
  client à ce que le serveur a fait.

Un X-Request-Id entrant est réutilisé plutôt que remplacé : une requête garde
un seul identifiant à travers un proxy ou entre services.

Ça prépare l'ingestion Loki sans dépendre de l'outil : le jour où le
collecteur arrive, il n'y a pas de code applicatif à reprendre.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ns les logs

Une plateforme de logs centralisée reçoit tous les projets et tous les
environnements au même endroit : sans ces champs, une ligne ne dit pas d'où
elle vient. Ajoutés à toutes les lignes JSON, omis du format terminal où ils
sont constants.

config.appname et config.version viennent du package.json du projet, avec
APP_NAME et APP_VERSION pour surcharger. appname était déjà utilisé par les
mails de crash mais jamais défini : leur objet s'intitulait « [undefined]
Crash: … ».

Les deux sont résolus à la lecture, pas à l'init : test/init.js — et tout
projet qui fait pareil — réassigne projectRoot après config.init().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
igo create <projet> --skel=front — SPA Vite + React + React Router +
TanStack Query, consommant l'API JSON d'igo.

Conforme aux ADR front : organisation par feature, règle d'injection des
données (seuls pages/ et sections/ appellent useQuery), TanStack Query pour
l'état serveur, tests Vitest + Testing Library + MSW.

Le proxy /api a été vérifié contre un vrai serveur igo : la requête traverse,
et surtout le cookie de session aussi — c'est le point que l'ADR chaîne de
build demandait de valider avant la première mise en production.

Le client HTTP lit les documents RFC 9457 : ApiError.fieldError(champ) donne
le message à afficher sous l'input concerné.

Le scaffold officiel Vite a servi de contrôle. Trois options reprises de lui,
dont erasableSyntaxOnly qui a trouvé une propriété de constructeur
non transpilable par esbuild.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tout ce qu'on refait à chaque nouveau projet, porté par le squelette :

- oxlint plutôt qu'eslint : typescript-eslint refuse TS 7 au chargement
  (« does not support TS 7.0 »), et npm rétrograde silencieusement en TS 6
  pour satisfaire son peer. oxlint n'a aucune dépendance au compilateur.
- husky + lint-staged : oxlint sur les fichiers indexés au pre-commit.
- commitlint : les Conventional Commits sont vérifiés, plus seulement écrits
  dans une doc.
- CI GitHub Actions : lint, typecheck, tests, build. Les services MySQL et
  Redis sont du boilerplate identique d'un projet igo à l'autre.
- CLAUDE.md : les conventions du projet, là où un agent les lira.
- .env.example, .nvmrc, pnpm-workspace.yaml.

Les squelettes passent à pnpm — son mode strict avait déjà trouvé le décalage
mocha 12. Il bloque aussi les postinstall par défaut, d'où les allowBuilds
pour esbuild, @parcel/watcher et msw.

create.js renomme maintenant aussi les dossiers `_.` : `_.husky/` et
`_.github/` restaient préfixés, donc inertes.

Les configs lint-staged vivent dans leur propre fichier, des deux côtés :
lint-staged remonte au plus proche package.json portant la clé, donc celui
d'un squelette s'appliquait aux commits d'igo et réclamait oxlint.

Vérifié à blanc sur les deux squelettes : lint, typecheck, tests et build
verts, et les deux hooks git bloquent bien ce qu'ils doivent bloquer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
igo create <projet> --skel=fullstack — back/ et front/ en workspaces pnpm
dans un dépôt, avec la racine qui démarre, teste et build les deux.

Un dépôt unique parce qu'un commit doit porter un front et un back cohérents,
et que le déploiement livre un seul artefact — c'est ce que tranche l'ADR
chaîne de build.

Playwright tourne contre le BUILD du front, pas le serveur de développement :
c'est ce qui est déployé. Vérifié de bout en bout — navigateur réel, proxy,
API igo, MySQL : 3 tests passent.

La CI reprend ce que ladom a appris : MySQL par docker run et non par
`services:`, qui ne sait pas passer les flags serveur — igo se connecte en
utf8mb4, et un serveur en latin1 casse au premier accent.

Deux pièges corrigés au passage :
- `vite preview` n'hérite pas de `server.proxy` : sans un bloc `preview`, le
  build servi aux E2E n'aurait aucune API derrière lui.
- vite écoute sur localhost (IPv6) ; l'URL en 127.0.0.1 que Playwright
  interroge n'était jamais joignable, d'où --host.

deploy/nginx.conf.example porte les deux réglages que l'ADR signale comme à ne
pas découvrir en production : index.html non caché, try_files pour le routage
client.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le formatage manquait, et c'est ce qui produit les débats de style en revue.
oxfmt plutôt que Prettier : même équipe et même parseur qu'oxlint, sortie
vérifiée identique à Prettier sur notre propre code, et les clés de
configuration sont celles de Prettier — basculer coûterait un renommage de
paquet.

Les scaffolds récents (Vite, Next) ne livrent aucun formateur ; NestJS livre
Prettier. Le choix se joue donc sur la cohérence d'outillage, et oxlint
recommande lui-même de sortir le formatage du linter au profit d'oxfmt.

Appliqué au pre-commit : oxfmt réécrit les fichiers indexés, oxlint valide,
et c'est la version formatée qui est commitée. La CI vérifie avec
format:check.

Conséquence assumée : l'alignement en colonnes du style igo disparaît des
projets générés. La frontière est nette — le framework garde son style, les
projets applicatifs suivent le formateur.

Les sources des trois squelettes sont formatées, donc un projet généré passe
format:check dès son premier commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… api

L'ADR organisation des sources distingue deux trajectoires : app/api/ pour
les refontes, app/features/ pour le greenfield, avec le modèle DANS la
feature. Les squelettes servent des projets neufs et suivaient pourtant la
structure de refonte — app/api/books/ à côté d'un app/models/ séparé.

Un domaine est maintenant auto-contenu : routes, contrôleur, DTO et modèle
côte à côte. Ce qui devient transversal migre dans shared/.

Le dossier back/ du squelette fullstack devient api/ : il contient une API,
et back ne se définissait que par opposition au front. Le nom aligne aussi
le fullstack sur le squelette autonome.

Vérifié à blanc sur les deux squelettes : format, lint, typecheck, 7 tests
back, 6 tests front, build, et 3 tests E2E contre la vraie chaîne.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MySQL passait par un docker run manuel avec attente explicite, là où Valkey
était déclaré en service. La justification reprise de ladom — « services: ne
sait pas passer de flags serveur » — est fausse : GitHub Actions expose une
clé command, et sa propre documentation l'illustre avec MySQL. Les deux sont
maintenant des services, health-checks compris.

Valkey remplace Redis : c'est ce qui tourne en production. Le client redis
d'igo parle le même protocole, vérifié contre une image valkey:8.

Le squelette front avait un job unique là où api en a trois, et figeait
node-version: 24 au lieu de lire .nvmrc. Les trois squelettes ont désormais
la même structure : lint, test, build.

Le squelette api documentait encore npm alors qu'il est passé à pnpm, et
aucun ne mentionnait le script format.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Les refontes partent de skel/front avec un back existant : sans Playwright,
chacune recâblait sa chaîne E2E. Le webServer sert le build et proxifie /api ;
API_URL désigne le back, que le squelette ne peut pas démarrer lui-même.

Le job E2E de la CI est désactivé tant que E2E_API_URL n'est pas renseigné —
un job rouge par défaut serait vite ignoré. Les seeds et l'authentification
restent l'affaire du projet, comme ladom a dû le construire.

Le job E2E attend lint et test, des deux côtés : c'est le plus lent et le plus
instable, inutile de le payer quand une erreur de typage dit déjà que le build
est cassé. lint et test restent parallèles, donc le retour rapide est préservé.

vitest ne ramasse plus e2e/ : ses specs importent @playwright/test, qu'il ne
sait pas résoudre.

Vérifié : les 2 E2E du squelette front passent contre une vraie API igo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le back d'une refonte vit dans le même dépôt : la chaîne E2E doit savoir le
lancer, pas supposer qu'il tourne déjà. playwright.config.ts démarre
maintenant l'API et le front, et les arrête après.

Le squelette front ne connaît pas la commande de démarrage du projet, donc
E2E_API_COMMAND est vide par défaut et se renseigne une fois. Le fullstack,
lui, connaît sa structure : il démarre `pnpm --filter ./api`.

En CI c'est le build qui est servi (`serve`), pas tsx watch — c'est ce qui est
déployé. En local, tsx watch évite un rebuild à chaque exécution.

L'URL de santé sondée était `/api`, à laquelle igo répond 404 : Playwright
n'attendait donc jamais que l'API soit prête, et échouait au bout de 120s.
Elle pointe sur une route qui répond vraiment.

Le job E2E du front attend aussi `build`. Le job build reste : les jobs ne
partagent pas de système de fichiers, donc chacun reconstruit — et comme le
job E2E est désactivé par défaut, sans lui le build ne serait pas vérifié.

Vérifié : 2 tests E2E côté front avec l'API démarrée par Playwright, 3 côté
fullstack, et 3 en mode CI contre le build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le job E2E arrivait désactivé, avec deux variables à câbler et un spec portant
sur un domaine « books » qui n'existera dans aucune refonte. Une refonte
devait supprimer le spec, câbler la commande de démarrage, écrire son seed et
gérer son authentification — plus de travail que `npx playwright init`.

Le squelette front ne peut pas savoir ce qu'il teste : son back a un métier
réel, que seul le projet connaît. Le fullstack garde Playwright, lui contrôle
les deux moitiés.

Vitest + Testing Library + MSW restent : ils couvrent les tests de composants,
la zone morte que l'ADR stratégie de test front identifie. Les E2E y sont
« chemins critiques seulement ».

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Un `- run: pnpm build` n'a pas besoin qu'on explique ce qu'il fait. Restent
les deux qui portent une raison non déductible : pourquoi les E2E tournent
contre le build, et qui démarre les serveurs.

Le premier était placé avant `pnpm migrate` alors qu'il parle du build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
getting-started ne mentionnait que --skel=api : front et fullstack existaient
sans que rien ne le dise. Un tableau comparatif dit lequel prendre — front
quand l'API existe déjà, fullstack pour démarrer les deux.

La feuille de route parlait encore d'un squelette unique et de app/api/ pour
le greenfield, que l'ADR réserve aux refontes.

Le job E2E migre avant de démarrer : auto_migrate n'est vrai qu'en production
et dev.test() ne migre que pour la suite de tests, donc une base CI fraîche
n'a aucune table sans ce step.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MySQL et Valkey en conteneurs, l'app tourne en natif — le modèle qu'utilise
déjà api-ceremonie chez funecap. Ça évite d'installer les deux à la main sur
chaque poste, et c'est identique d'un projet igo à l'autre.

Le flag utf8mb4 y est aussi : sans lui, MySQL démarre sur son charset par
défaut et casse au premier accent.

Pas de Dockerfile : ladom et certigo déploient par Ansible sur du bare metal,
funecap pousse des images sur ECR. Le squelette ne peut pas trancher, et un
Dockerfile faux est pire que pas de Dockerfile.

Vérifié : docker compose up -d démarre les deux conteneurs sains, le serveur
est bien en utf8mb4, et les 7 tests du squelette passent contre eux.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mickael-coquer-igocreate

Copy link
Copy Markdown
Contributor Author

Code review

Found 2 issues:

  1. Le squelette fullstack documente le chemin d'un domaine côté API comme api/app/api/<domaine>/, alors que la structure réellement générée (et api/CLAUDE.md:23) utilise api/app/features/<domaine>/. Un développeur suivant ce doc placera son code dans un répertoire inexistant.

```
api/app/api/<domaine>/ routes, controller, dto
front/src/features/<domaine>/ api.ts, types.ts, pages, sections, components
```

  1. Le squelette fullstack renvoie trois fois vers un front/CLAUDE.md qui n'existe pas dans skel/fullstack/front/ (seul skel/front/CLAUDE.md existe, et cli/create.js ne fait que renommer les fichiers _.-préfixés, il n'en crée aucun). Tout projet fullstack généré part donc avec une référence morte.

**Les conventions de code sont dans `api/CLAUDE.md` et `front/CLAUDE.md`.** Ce
fichier ne couvre que ce qui concerne les deux.

api/ API igo — voir api/CLAUDE.md
front/ SPA React — voir front/CLAUDE.md
e2e/ parcours Playwright

https://github.com/igocreate/igo/blob/8a615e11474e3f3922cef441adf892446f12c2eb/packages/server/skel/fullstack/README.md#L75-L77

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Le CLAUDE.md et la doc prescrivent un `type` par cas métier, mais les 404
du contrôleur sortaient un `about:blank` : le squelette contredisait la
convention qu'il est censé enseigner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`igo db seed` filtrait sur `.js` : dans un projet TS, le dossier seeds/
était traité comme vide, sans erreur. Un module TS exporte via `default`,
d'où le repli.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Le squelette api n'avait aucun moyen documenté de créer ses tables, et
seul fullstack exposait migrate — à sa racine, pas dans api/.

Le seed importe les modèles TypeScript, d'où le loader tsx.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…uelettes

- logging.md : plus de request_id ni de X-Request-Id ; req.traceId, traceparent
  en entrée, traceresponse en sortie, trace_id sur les logs, LOG_REQUESTS et le
  plancher de statut, redact() et config.sensitiveKeys
- getting-started.md et CLAUDE.md : deux squelettes, tailwind et fullstack ;
  plus d'exemple nginx
- api.md : app/features/<domaine>/, comme le squelette
- ADR : @axe-core/playwright remplace axe-playwright, trace_id remplace requestId

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…'ORM, retraits

- l'écran d'exemple, ses tests et le page object E2E passent en français,
  comme le déclare déjà `lang="fr"` ; les libellés de test aussi : ils
  décrivent un comportement du domaine, les blocs describe techniques restent
- @igojs/db reçoit un index.d.ts minimal : Model<Row>(schema) rend une classe
  dont find/create/where/page/list sont typés ; Book.ts passe en import et
  Book.find() rend Instance<BookRow> | null, ce qu'un test a aussitôt révélé
- BookId (params) sur show, update et destroy : /api/books/abc répond 400, et
  la troisième source de validation a un exemple ; un test
- formulaire : erreur globale avec role="alert" et la référence de trace quand
  aucun champ n'est visé, aria-invalid et aria-describedby sur les champs en
  erreur, inputMode numérique ; un test front pour le 500
- @igojs/igo retiré de api/package.json (rien ne l'importe), locales/ retiré
  (vestige dust, igo démarre sans), tsconfig.build.json pour que dist/ ne
  contienne ni test/ ni seeds/
- commentaires faux, périmés ou paraphrases retirés : le back « en
  JavaScript », e2e « sous front/ », le grep de front/CLAUDE.md que son
  propre rappel faisait échouer, et les redites du README et des CLAUDE.md

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Six gestes vérifiés sur un projet tailwind en JavaScript : dépendances directes
@igojs/server et @igojs/db, tsx et un tsconfig avec allowJs, la feature copiée,
le modèle JS décrit par un .d.ts sans le modifier, le montage avec .default, le
démarrage et les tests sous tsx.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ns traceur

- middleware d'en-têtes piloté par config.security : nosniff, X-Frame-Options
  SAMEORIGIN, Referrer-Policy, Permissions-Policy, HSTS en production sur
  HTTPS ; sur les requêtes API, CSP stricte et Cache-Control: no-store ; pas de
  CSP par défaut sur les pages, elle est faite des exceptions du projet. Les
  valeurs sont celles qu'un pentest a acceptées sur ladom (DCO-2306, 2311, 2315)
- squelette : la CSP de la SPA est posée en <meta> par vite.config.ts, stricte
  au build, assouplie en développement pour le préambule de Vite et HMR,
  connect-src étendu à l'origine de Faro ; frame-ancestors et HSTS restent à
  nginx. Vérifiée par les E2E contre le build et contre le serveur de dev
- Faro : sessionTracking désactivé, plus d'identifiant écrit dans le
  navigateur avant consentement ; erreurs, Web Vitals et corrélation
  front/back inchangés
- documentation : Production › Security Headers, front/CLAUDE.md (sécurité,
  données personnelles), feuille de route (helmet à rechallenger en v7)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…nais

Les README s'adressent aux humains — démarrer, commandes, structure,
conventions — et sont la seule source ; chaque paquet a le sien. Les CLAUDE.md
importent le README voisin (@README.md) et n'ajoutent que ce que l'assistant
doit vérifier ou ne pas défaire : contrôles avant livraison, règles d'injection
et de validation, interrupteurs d'observabilité, CSP, données propres aux
tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… framework

- deploy/nginx.conf.example et deploy/config.alloy.example : des points de
  départ, pas des fichiers actifs ; le README énonce le contrat de production
  (index.html jamais en cache, try_files, /api proxifié, frame-ancestors et
  HSTS posés par nginx, OTLP vers un collecteur local). Extensible à Ansible
- .github/dependabot.yml : mineures et correctifs groupés, une PR par mois
- feuille de route : l'authentification fournie par igo, candidat v7
- commentaires du framework ramenés à ce que le code ne dit pas : env.js,
  redact.js, config.js, handler.d.ts, requestlogger.js

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
requireSession répond 401 (/problems/unauthenticated) quand personne n'est en
session ; DELETE /api/books/:id l'utilise, avec le test d'accès refusé que l'ADR
exige et le cas passant via la session de dev.agent. La garde est générique sur
les paramètres de route, sans quoi Express ne trouve pas de surcharge devant un
handler à schéma params — motif verrouillé dans test/types/valid.ts. Ce que le
projet fait de req.session.userId lui appartient ; le parcours de connexion est
le chantier v7 de la feuille de route.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e en CI

Première exécution du workflow du squelette sur GitHub : pnpm/action-setup@v4
exige la version de pnpm, dans le workflow ou dans package.json. Les actions
checkout et setup-node passent en v5, la v4 ciblant un Node 20 déprécié sur les
runners.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-node 7, pnpm 6, upload-artifact 7)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`GET /health` dit que le process répond, `GET /health/ready` sonde la base, le
cache et l'espace disque, et rend 503 lorsque l'un des trois manque — un
répartiteur de charge ne lit que le code, c'est lui qui sort l'instance du
service.

Format Actuator (`status`, `components`) plutôt que le `health+json` du draft
IETF, expiré en 2022 et jamais repris : la séparation liveness/readiness est ce
qui s'est standardisé, pas le corps de la réponse.

Le motif d'échec reste dans les logs : la route est joignable par qui joint le
service, et une erreur de connexion nomme des hôtes et des ports. Les deux
routes sont montées avant le journal de requête, qu'elles rempliraient sinon.

Processeur et mémoire ne sont pas sondés : un processeur saturé est souvent une
instance qui travaille, et la sortir du service reporterait la charge sur les
autres.

Côté squelette, `GET /` disparaît et les E2E attendent `/health/ready`, qui
n'est un 2xx que lorsque les dépendances répondent.
…ness

Redis absent rendait 503, donc sortait du répartiteur de charge une instance
qui servait encore : igo dégrade sans cache, il ne s'arrête pas. Une panne
fabriquée à partir d'une dégradation.

Une sonde vaut désormais `'optional'` — signalée dans `components`, sans effet
sur le statut global ni sur le code de réponse. Le cache l'est par défaut, la
base et le disque restent critiques : sans eux, l'application ne peut plus
servir ni écrire.
…ession

`sessionTracking: false` rendait le front muet : le collecteur Grafana Cloud
répond 400 sans en-tête `X-Faro-Session-Id`, erreurs comprises. La démo n'a plus
rien remonté pendant vingt heures sans que rien ne le signale.

`persistent: false` garde l'identifiant en mémoire — il meurt avec l'onglet et
rien n'est écrit dans le navigateur, donc pas de traceur au sens de l'article 82
de la loi Informatique et Libertés. Ce qu'on y perd : les parcours multi-pages,
qu'un rechargement coupe.

Le README du front l'explique, et pose une convention que le regroupement des
erreurs par Grafana impose : un message nomme la classe du problème, jamais
l'occurrence.
`config.alloy.example` mêlait deux rôles qu'une contrainte sépare : un fichier
de log et un `/proc` ne se lisent que localement, un `/metrics` se scrute de
n'importe où.

D'où `config.alloy.agent.example`, déployé par machine — y compris celles qui ne
portent qu'une base, pour leurs métriques système — et
`config.alloy.gateway.example`, déployé une fois. C'est la passerelle qui absorbe
la variabilité des hébergeurs : le tableau de bord interroge `mysql_*`, `redis_*`
et `node_*` quelle que soit la plateforme, et elle va les chercher là où elles
sont. Un projet chez OVH ou Scaleway réécrit ce fichier, pas ses tableaux de bord.

Les deux voies rencontrées y sont documentées : OVHcloud expose un endpoint
Prometheus classique, Scaleway passe par Cockpit depuis le retrait des endpoints
par instance. Ni l'un ni l'autre ne documente ses noms de métriques.
…moire

Trois réglages que trois pannes provoquées ont rendus nécessaires.

`SONDE_URL` — une application morte n'émet plus rien : la série disparaît au lieu
de valoir zéro, et rien ne se déclenche. Mesuré, cinq minutes d'API arrêtée
n'avaient ému aucune des treize règles. La sonde de la passerelle interroge la
readiness et porte alors l'information.

`METRIQUES_INSTANCE` et `METRIQUES_ROLE` — sans la première, `instance` vaut le
nom d'hôte du conteneur, que Docker tire au sort : trois recréations ont laissé
trois jeux de sept cents séries. La seconde range processeur et mémoire sous le
service que la machine porte.

`maxmemory` sur Valkey — sans plafond il grossit jusqu'à ce que le noyau tue le
process, et la mémoire ne se lit en pourcentage de rien.
Le tableau de bord suit les quatre signaux dorés en tête — trafic, erreurs,
latence, saturation — puis le détail HTTP, les journaux, et une rangée repliée
par service avec la machine qui le porte.

Chaque requête a été vérifiée contre des données réelles plutôt que déduite des
noms attendus : la pile pousse des métriques OTel (`http_server_request_duration`
et non `http_request_duration`), et c'est cette supposition qui avait coûté un
tableau de bord entier.

Quatorze règles, dont deux livrées désactivées faute de valoir pour tous les
projets : le seuil de latence absolue, qui se règle après quelques semaines de
mesures, et l'effondrement du trafic, qui sonnerait chaque nuit sur un service à
creux nocturne. Le README dit ce que chacune signale et par où commencer.

Deux d'entre elles ne pouvaient pas se déclencher : une comparaison PromQL est un
filtre qui conserve la valeur d'origine, donc `probe_success < 1` rendait zéro et
`predict_linear(...) < 0` une valeur négative — que la condition « supérieur à
zéro » de Grafana ne voit jamais. Inversées, elles rendent ce qui manque.
Comment thread packages/server/cli/db.js
for (const file of files) {
const seed = require(path.join(seedsDir, file));
await seed();
await (seed.default || seed)();

@mickael-coquer-igocreate mickael-coquer-igocreate Sep 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arnaudm le default est là pour la compatiblité avec un seed en Typescript qui fera export default

`directory: /` ne voit qu'un fichier, et celui de la racine ne porte que
l'outillage — commitlint, husky, oxlint. Express, React, igo, Playwright et
OpenTelemetry vivent dans les espaces de travail, où rien ne les regardait.

Le README dit aussi ce qu'aucun fichier versionné ne peut porter : les mises à
jour de sécurité sont un réglage du dépôt, à activer à la création. Sans lui, un
projet a le fichier sans les alertes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`requireSession` décrivait le moyen. `requireAuth` dit ce qu'on attend d'elle, et
laisse la place à une garde d'autorisation qui viendra à côté plutôt qu'à sa
place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les exceptions arrivaient sans trace, contrairement aux évènements : elles
surviennent hors d'un span, et celui du fetch est déjà clos quand le catch
s'exécute — `getActiveSpan()` ne rend donc rien.

`ApiError` portait déjà le trace-id lu dans l'en-tête `traceresponse` ; il
suffisait de le passer à Faro. Depuis une erreur du navigateur on retrouve
maintenant les spans serveur et les lignes de log qui portent le même
identifiant.

Ne couvre que ce qui découle d'un appel : une erreur de rendu ou un rejet non
géré n'est lié à aucune requête, il n'y a rien à rattacher.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`arbre`, `estUneRequete`, `attendu` et `resultats` désignent des mécanismes, pas
des objets du domaine : la règle de langue du projet les veut en anglais.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le gestionnaire d'erreurs portait des paraphrases — « Send response » devant un
`res.status()`, « Check if response already sent » devant `res.headersSent` — et
un commentaire justifiant de passer la stack en second argument de Winston, ce
que personne n'aurait fait autrement.

Les configurations Alloy gardent les faits mesurés et perdent les
raisonnements : la section qui expliquait l'absence d'un connecteur
`spanmetrics` documentait du code qui n'est pas là.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`describeCliError` préfixait les erreurs de connexion du CLI avec l'hôte, le port
et le nom de la base, en lisant `config.mysql` : un projet sur PostgreSQL en
sortait un message faux, et la liste de codes attrapait des `ECONNREFUSED` qui
ne venaient pas d'une base.

Quinze lignes pour un confort que le pilote rend déjà : mysql2 comme pg mettent
l'hôte et le port dans leur message. Seul le nom de la base manque, et il ne
vaut pas qu'un fichier de gestion d'erreur sache comment un projet déclare ses
bases.

`failCli` reste : sans lui, une commande CLI qui échoue tente d'envoyer un
courriel de crash et cherche un contexte de requête qui n'existe pas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La fonction répond à « cette requête est-elle servie en JSON ? » — une question
de routage. Elle vivait dans `api/problem.js` par accident : c'est le premier
endroit qui en avait eu besoin.

Le symptôme était le middleware d'en-têtes de sécurité, qui importait le module
des documents RFC 9457 pour savoir s'il devait poser une CSP. Aucun des quatre
appelants — en-têtes, `unlessApi()`, le 404, le gestionnaire d'erreurs — ne parle
de documents de problème.

`api/request.js` n'a pas de dépendance et s'importe depuis n'importe où.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…types

Le test se sautait quand `tsc` était introuvable, et annonçait donc des
déclarations vérifiées alors que rien ne les avait lues. TypeScript est une
devDependency de l'espace de travail : son absence est une installation cassée,
pas une raison de passer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`X-Request-Id` a vécu sept jours dans cette branche avant de céder la place à
`traceresponse`. Un test affirmait son absence et un commentaire l'expliquait :
un aller-retour interne, invisible de l'extérieur, qu'aucune version n'a porté.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
La liste des middlewares parlait encore d'identifiant de requête, quand
`logging.md` décrit déjà le trace id du W3C Trace Context.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`traceIdDeLaReponse` et `entete` désignent un mécanisme HTTP, pas un objet du
domaine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mickael-coquer-igocreate
mickael-coquer-igocreate merged commit fbfffa7 into master Sep 17, 2026
2 of 4 checks passed
@mickael-coquer-igocreate
mickael-coquer-igocreate deleted the feat/api-first-phase1 branch September 17, 2026 13:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants