[{"data":1,"prerenderedAt":2949},["ShallowReactive",2],{"authors":3,"article-2026-06-09-generation-de-types-depuis-routes-fastify":331},[4,23,35,48,61,73,85,98,111,124,136,148,161,173,185,197,209,221,233,245,258,270,282,295,307,319],{"id":5,"title":6,"body":7,"description":11,"extension":14,"meta":15,"name":16,"navigation":17,"path":18,"readingTime":19,"seo":20,"stem":21,"__hash__":22},"authors\u002Fauthors\u002Falexandre-guillon.md","Software Engineer",{"type":8,"value":9,"toc":10},"minimark",[],{"title":11,"searchDepth":12,"depth":12,"links":13},"",2,[],"md",{},"Alexandre Guillon",true,"\u002Fauthors\u002Falexandre-guillon",1,{"title":6,"description":11},"authors\u002Falexandre-guillon","4tf48mjyjFNqItOHaulICbrjeCyMag1o6801uHeTz98",{"id":24,"title":6,"body":25,"description":11,"extension":14,"meta":29,"name":30,"navigation":17,"path":31,"readingTime":19,"seo":32,"stem":33,"__hash__":34},"authors\u002Fauthors\u002Falexis-ablain.md",{"type":8,"value":26,"toc":27},[],{"title":11,"searchDepth":12,"depth":12,"links":28},[],{},"Alexis Ablain","\u002Fauthors\u002Falexis-ablain",{"title":6,"description":11},"authors\u002Falexis-ablain","_SIAtB7f-39e5t3GiJof81NP47s6MGo2n4gaHkTy1uQ",{"id":36,"title":37,"body":38,"description":11,"extension":14,"meta":42,"name":43,"navigation":17,"path":44,"readingTime":19,"seo":45,"stem":46,"__hash__":47},"authors\u002Fauthors\u002Faxel-shaita.md","Engineering Manager",{"type":8,"value":39,"toc":40},[],{"title":11,"searchDepth":12,"depth":12,"links":41},[],{},"Axel Shaïta","\u002Fauthors\u002Faxel-shaita",{"title":37,"description":11},"authors\u002Faxel-shaita","fK0argUhsBkWLjpTAhY13oYLVzQthcEYkCEdtHWmIgE",{"id":49,"title":50,"body":51,"description":11,"extension":14,"meta":55,"name":56,"navigation":17,"path":57,"readingTime":19,"seo":58,"stem":59,"__hash__":60},"authors\u002Fauthors\u002Fbaptiste-faure.md","Head of Talent Acquisition",{"type":8,"value":52,"toc":53},[],{"title":11,"searchDepth":12,"depth":12,"links":54},[],{},"Baptiste Faure","\u002Fauthors\u002Fbaptiste-faure",{"title":50,"description":11},"authors\u002Fbaptiste-faure","ELisToYtcgHmgdVWZkCclTPV6exZtfyXqhpx1jjbJHs",{"id":62,"title":6,"body":63,"description":11,"extension":14,"meta":67,"name":68,"navigation":17,"path":69,"readingTime":19,"seo":70,"stem":71,"__hash__":72},"authors\u002Fauthors\u002Fbenjamin-bouillot.md",{"type":8,"value":64,"toc":65},[],{"title":11,"searchDepth":12,"depth":12,"links":66},[],{},"Benjamin Bouillot","\u002Fauthors\u002Fbenjamin-bouillot",{"title":6,"description":11},"authors\u002Fbenjamin-bouillot","tbhCFZyfTt7ZM5b5YgqQ2nhgnSTl8BweaQQryc87fHo",{"id":74,"title":37,"body":75,"description":11,"extension":14,"meta":79,"name":80,"navigation":17,"path":81,"readingTime":19,"seo":82,"stem":83,"__hash__":84},"authors\u002Fauthors\u002Fcedric-nicoloso.md",{"type":8,"value":76,"toc":77},[],{"title":11,"searchDepth":12,"depth":12,"links":78},[],{},"Cédric Nicoloso","\u002Fauthors\u002Fcedric-nicoloso",{"title":37,"description":11},"authors\u002Fcedric-nicoloso","ibSoh4VZYiWYTuLOnZTedaAfcnvet1Q9H7ogW0LgorY",{"id":86,"title":87,"body":88,"description":11,"extension":14,"meta":92,"name":93,"navigation":17,"path":94,"readingTime":19,"seo":95,"stem":96,"__hash__":97},"authors\u002Fauthors\u002Fdavid-touzet.md","Staff Engineer",{"type":8,"value":89,"toc":90},[],{"title":11,"searchDepth":12,"depth":12,"links":91},[],{},"David Touzet","\u002Fauthors\u002Fdavid-touzet",{"title":87,"description":11},"authors\u002Fdavid-touzet","dHWwnQxb1Ubt-WwXWEODGEo9AFoq1cJUhfg3kdnYSBM",{"id":99,"title":100,"body":101,"description":11,"extension":14,"meta":105,"name":106,"navigation":17,"path":107,"readingTime":19,"seo":108,"stem":109,"__hash__":110},"authors\u002Fauthors\u002Feloise-chizat.md","Data Engineer",{"type":8,"value":102,"toc":103},[],{"title":11,"searchDepth":12,"depth":12,"links":104},[],{},"Eloïse Chizat","\u002Fauthors\u002Feloise-chizat",{"title":100,"description":11},"authors\u002Feloise-chizat","Utd72Vm9qT4hh2ZbFi6a2_nXw5Wb494Ed_HL1ra5yw8",{"id":112,"title":113,"body":114,"description":11,"extension":14,"meta":118,"name":119,"navigation":17,"path":120,"readingTime":19,"seo":121,"stem":122,"__hash__":123},"authors\u002Fauthors\u002Femmanuel-auclair.md","Staff engineer",{"type":8,"value":115,"toc":116},[],{"title":11,"searchDepth":12,"depth":12,"links":117},[],{},"Emmanuel Auclair","\u002Fauthors\u002Femmanuel-auclair",{"title":113,"description":11},"authors\u002Femmanuel-auclair","MtsA8THNLEn0dTtYEIQaGwDuf7MjQL55IOeei5gugEg",{"id":125,"title":6,"body":126,"description":11,"extension":14,"meta":130,"name":131,"navigation":17,"path":132,"readingTime":19,"seo":133,"stem":134,"__hash__":135},"authors\u002Fauthors\u002Fhoreb-parraud.md",{"type":8,"value":127,"toc":128},[],{"title":11,"searchDepth":12,"depth":12,"links":129},[],{},"Horeb Parraud","\u002Fauthors\u002Fhoreb-parraud",{"title":6,"description":11},"authors\u002Fhoreb-parraud","ajjsnUX4ohZI-ghMdbb92q_taWDkKXVZSLZXoAeLQtg",{"id":137,"title":37,"body":138,"description":11,"extension":14,"meta":142,"name":143,"navigation":17,"path":144,"readingTime":19,"seo":145,"stem":146,"__hash__":147},"authors\u002Fauthors\u002Fhugo-contreras.md",{"type":8,"value":139,"toc":140},[],{"title":11,"searchDepth":12,"depth":12,"links":141},[],{},"Hugo Contreras","\u002Fauthors\u002Fhugo-contreras",{"title":37,"description":11},"authors\u002Fhugo-contreras","2nc3VMu9ASq9Z6Pwx2-7-Ye991Pww4p-UEDBQFfjF-Q",{"id":149,"title":150,"body":151,"description":11,"extension":14,"meta":155,"name":156,"navigation":17,"path":157,"readingTime":19,"seo":158,"stem":159,"__hash__":160},"authors\u002Fauthors\u002Fjulien-tassin.md","Head of Engineering",{"type":8,"value":152,"toc":153},[],{"title":11,"searchDepth":12,"depth":12,"links":154},[],{},"Julien Tassin","\u002Fauthors\u002Fjulien-tassin",{"title":150,"description":11},"authors\u002Fjulien-tassin","iUIHI7SITje38Jh9X9uvYs4-VsHx4eCdt6hAlyLFG_o",{"id":162,"title":6,"body":163,"description":11,"extension":14,"meta":167,"name":168,"navigation":17,"path":169,"readingTime":19,"seo":170,"stem":171,"__hash__":172},"authors\u002Fauthors\u002Flaurent-renard.md",{"type":8,"value":164,"toc":165},[],{"title":11,"searchDepth":12,"depth":12,"links":166},[],{},"Laurent Renard","\u002Fauthors\u002Flaurent-renard",{"title":6,"description":11},"authors\u002Flaurent-renard","5BP7Ed-pt1SQHjh0UJ1XUrlLTcdlFaDoKBCP4deHq8A",{"id":174,"title":6,"body":175,"description":11,"extension":14,"meta":179,"name":180,"navigation":17,"path":181,"readingTime":19,"seo":182,"stem":183,"__hash__":184},"authors\u002Fauthors\u002Fleo-martin.md",{"type":8,"value":176,"toc":177},[],{"title":11,"searchDepth":12,"depth":12,"links":178},[],{},"Léo Martin","\u002Fauthors\u002Fleo-martin",{"title":6,"description":11},"authors\u002Fleo-martin","eYxCHkRgbGDV7shKdTA9s7Tu0zGV4yDGFoKR5MHQntY",{"id":186,"title":6,"body":187,"description":11,"extension":14,"meta":191,"name":192,"navigation":17,"path":193,"readingTime":19,"seo":194,"stem":195,"__hash__":196},"authors\u002Fauthors\u002Floic-bousquet.md",{"type":8,"value":188,"toc":189},[],{"title":11,"searchDepth":12,"depth":12,"links":190},[],{},"Loïc Bousquet","\u002Fauthors\u002Floic-bousquet",{"title":6,"description":11},"authors\u002Floic-bousquet","ko12qZwiGL8XNjAoy9oWypPkIjr29Pbq7vhdtgldqeQ",{"id":198,"title":6,"body":199,"description":11,"extension":14,"meta":203,"name":204,"navigation":17,"path":205,"readingTime":19,"seo":206,"stem":207,"__hash__":208},"authors\u002Fauthors\u002Floic-poullain.md",{"type":8,"value":200,"toc":201},[],{"title":11,"searchDepth":12,"depth":12,"links":202},[],{},"Loïc Poullain","\u002Fauthors\u002Floic-poullain",{"title":6,"description":11},"authors\u002Floic-poullain","oRIyJhFRTqxy5dLCYQ2OnYZ1DB-gLDUM-85vTSYuTF0",{"id":210,"title":100,"body":211,"description":11,"extension":14,"meta":215,"name":216,"navigation":17,"path":217,"readingTime":19,"seo":218,"stem":219,"__hash__":220},"authors\u002Fauthors\u002Fmaud-lelu.md",{"type":8,"value":212,"toc":213},[],{"title":11,"searchDepth":12,"depth":12,"links":214},[],{},"Maud Lélu","\u002Fauthors\u002Fmaud-lelu",{"title":100,"description":11},"authors\u002Fmaud-lelu","MMbsCKuE41OMHusrl12FIEsI-Trx7l8Nn_ANhvj2_y4",{"id":222,"title":37,"body":223,"description":11,"extension":14,"meta":227,"name":228,"navigation":17,"path":229,"readingTime":19,"seo":230,"stem":231,"__hash__":232},"authors\u002Fauthors\u002Fnicolas-poirier.md",{"type":8,"value":224,"toc":225},[],{"title":11,"searchDepth":12,"depth":12,"links":226},[],{},"Nicolas Poirier","\u002Fauthors\u002Fnicolas-poirier",{"title":37,"description":11},"authors\u002Fnicolas-poirier","dXrJkYo8az4SN_D23aYc3fQ7z8s1dR2a0lt1ogjAjJs",{"id":234,"title":37,"body":235,"description":11,"extension":14,"meta":239,"name":240,"navigation":17,"path":241,"readingTime":19,"seo":242,"stem":243,"__hash__":244},"authors\u002Fauthors\u002Fraphael-sauget.md",{"type":8,"value":236,"toc":237},[],{"title":11,"searchDepth":12,"depth":12,"links":238},[],{},"Raphaël Sauget","\u002Fauthors\u002Fraphael-sauget",{"title":37,"description":11},"authors\u002Fraphael-sauget","Uri9bcq0QDuxRA0PbBoNtu7p_5L3dALu4kzcXVW0xyM",{"id":246,"title":247,"body":248,"description":11,"extension":14,"meta":252,"name":253,"navigation":17,"path":254,"readingTime":19,"seo":255,"stem":256,"__hash__":257},"authors\u002Fauthors\u002Fromain-koenig.md","Co-funder & Head of innovation",{"type":8,"value":249,"toc":250},[],{"title":11,"searchDepth":12,"depth":12,"links":251},[],{},"Romain Koenig","\u002Fauthors\u002Fromain-koenig",{"title":247,"description":11},"authors\u002Fromain-koenig","uyS8--eG2_ezyqRABcJnMJmQKKuSArhPWd14aUvFeEw",{"id":259,"title":37,"body":260,"description":11,"extension":14,"meta":264,"name":265,"navigation":17,"path":266,"readingTime":19,"seo":267,"stem":268,"__hash__":269},"authors\u002Fauthors\u002Fromaric-juniet.md",{"type":8,"value":261,"toc":262},[],{"title":11,"searchDepth":12,"depth":12,"links":263},[],{},"Romaric Juniet","\u002Fauthors\u002Fromaric-juniet",{"title":37,"description":11},"authors\u002Fromaric-juniet","4Zb2artgT-eo-PHLXi3xi4d5t7s6PfhUxeSfXIikSUY",{"id":271,"title":6,"body":272,"description":11,"extension":14,"meta":276,"name":277,"navigation":17,"path":278,"readingTime":19,"seo":279,"stem":280,"__hash__":281},"authors\u002Fauthors\u002Fstanyslas-bres.md",{"type":8,"value":273,"toc":274},[],{"title":11,"searchDepth":12,"depth":12,"links":275},[],{},"Stanyslas Bres","\u002Fauthors\u002Fstanyslas-bres",{"title":6,"description":11},"authors\u002Fstanyslas-bres","Xa0SahETuiN4q1jrmR2ych3moAqcZ2LbU7vSfEt2RuU",{"id":283,"title":284,"body":285,"description":11,"extension":14,"meta":289,"name":290,"navigation":17,"path":291,"readingTime":19,"seo":292,"stem":293,"__hash__":294},"authors\u002Fauthors\u002Ftalent-acquisition.md","Talent Acquisition",{"type":8,"value":286,"toc":287},[],{"title":11,"searchDepth":12,"depth":12,"links":288},[],{},"Équipe Talent Acquisition","\u002Fauthors\u002Ftalent-acquisition",{"description":11},"authors\u002Ftalent-acquisition","doDfE76txftQ4wIiKjJoDmSpyzSKk0tzlgVAp6-opAY",{"id":296,"title":6,"body":297,"description":11,"extension":14,"meta":301,"name":302,"navigation":17,"path":303,"readingTime":19,"seo":304,"stem":305,"__hash__":306},"authors\u002Fauthors\u002Fvictor-borg.md",{"type":8,"value":298,"toc":299},[],{"title":11,"searchDepth":12,"depth":12,"links":300},[],{},"Victor Borg","\u002Fauthors\u002Fvictor-borg",{"title":6,"description":11},"authors\u002Fvictor-borg","-Za-JweoiP6hyclue_WkxMXdRUDTczPGlJf6AZckjUc",{"id":308,"title":6,"body":309,"description":11,"extension":14,"meta":313,"name":314,"navigation":17,"path":315,"readingTime":19,"seo":316,"stem":317,"__hash__":318},"authors\u002Fauthors\u002Fvirgil-roger.md",{"type":8,"value":310,"toc":311},[],{"title":11,"searchDepth":12,"depth":12,"links":312},[],{},"Virgil Roger","\u002Fauthors\u002Fvirgil-roger",{"title":6,"description":11},"authors\u002Fvirgil-roger","DfVFe5j0bCgXeEr381ZYOM5DP4m-pWb93J9-m_muKJ0",{"id":320,"title":6,"body":321,"description":11,"extension":14,"meta":325,"name":326,"navigation":17,"path":327,"readingTime":19,"seo":328,"stem":329,"__hash__":330},"authors\u002Fauthors\u002Fyukan-zhao.md",{"type":8,"value":322,"toc":323},[],{"title":11,"searchDepth":12,"depth":12,"links":324},[],{},"Yukan Zhao","\u002Fauthors\u002Fyukan-zhao",{"title":6,"description":11},"authors\u002Fyukan-zhao","LRPHugtAJnWHsmHxy9_SR5Zas_C5p-GR_uHEs1Fhk_E",{"id":332,"title":333,"author":334,"body":335,"date":2939,"description":2940,"extension":14,"lang":2941,"meta":2942,"navigation":17,"path":2943,"published":17,"readingTime":719,"seo":2944,"stem":2945,"tags":2946,"__hash__":2948},"articles\u002Farticles\u002F2026-06-09-generation-de-types-depuis-routes-fastify.md","Comment nos schémas Fastify sont devenus des types pour nos appels API","stanyslas-bres",{"type":8,"value":336,"toc":2915},[337,341,348,354,362,377,384,390,395,398,426,435,438,450,454,459,462,465,468,471,475,478,481,489,492,503,509,513,516,519,522,530,545,552,559,563,566,577,580,610,614,617,757,761,768,771,866,872,875,893,897,903,906,909,953,956,960,963,994,997,1000,1340,1343,1486,1490,1493,1496,1676,1679,1687,1691,1702,1705,1708,2042,2045,2140,2143,2197,2200,2232,2241,2246,2249,2254,2258,2261,2264,2335,2338,2341,2344,2425,2428,2432,2438,2441,2444,2447,2453,2456,2467,2470,2474,2477,2492,2499,2502,2505,2519,2522,2526,2529,2532,2535,2563,2566,2620,2623,2652,2655,2702,2705,2763,2766,2770,2796,2800,2852,2856,2859,2894,2898,2901,2911],[338,339,340],"p",{},"Pendant longtemps, notre client principal était une application Vue 2 + JavaScript. Tous les appels\nréseau n'étaient pas typés, le contrat était implicite, et on découvrait souvent les écarts au\nruntime quand la validation échouait, pas idéal.",[338,342,343,344,347],{},"On ne pouvait pas y faire grand chose tant que le client restait majoritairement écrit en\nJavaScript.",[345,346],"br",{},"\nMais au moment de migrer vers Vue 3 et TypeScript, une question est devenue centrale :",[338,349,350],{},[351,352,353],"strong",{},"Comment typer proprement les réponses de nos APIs pour éviter au maximum les erreurs au runtime\n?",[338,355,356,357,361],{},"Nos APIs exposaient déjà de nombreuses routes Fastify avec des schémas de validation ",[358,359,360],"code",{},"json-schema",".",[338,363,364,365,368,369,372,373,376],{},"Ces schémas servaient d'abord au runtime : valider les ",[358,366,367],{},"params",", la ",[358,370,371],{},"querystring",", le ",[358,374,375],{},"body",", et\nparfois documenter les réponses. Ils existaient déjà sur une grande partie de l'API, y compris sur\ncertains routeurs legacy encore écrits en JavaScript.",[338,378,379,380,383],{},"Notre intuition était donc simple : plutôt que d'inventer une nouvelle source de vérité côté client,\npouvait-on ",[351,381,382],{},"réutiliser ces schémas serveur pour produire des types TypeScript"," ?",[385,386,387],"blockquote",{},[338,388,389],{},"L'objectif n'était pas de générer un SDK complet, ni de remplacer notre client HTTP. On voulait un\nsystème type-only : effacé à la compilation et sans impact sur la taille du bundle.",[391,392,394],"h2",{"id":393},"le-problème-typer-une-api-déjà-existante","Le problème : typer une API déjà existante",[338,396,397],{},"Ce que nous avions déjà pour la majorité des routes API :",[399,400,401,405,408,411,414,417,420,423],"ul",{},[402,403,404],"li",{},"un schéma de validation pour les paramètres d'URL ;",[402,406,407],{},"un schéma de validation pour la query string ;",[402,409,410],{},"un schéma de validation pour le body ;",[402,412,413],{},"un schéma de réponse (quand l'équipe a pris le temps de le déclarer) ;",[402,415,416],{},"des handlers Fastify existants ;",[402,418,419],{},"des routeurs legacy encore en JavaScript ;",[402,421,422],{},"un client web en pleine migration vers TypeScript ;",[402,424,425],{},"une application mobile qui devait aussi consommer ces contrats.",[385,427,428],{},[338,429,430,431,434],{},"Le risque, en basculant vers TypeScript, était de créer ",[351,432,433],{},"une deuxième vérité côté client"," : des\ntypes écrits à la main dans le client, plus ou moins synchronisés avec les schémas serveur.",[338,436,437],{},"On avait donc ces trois objectifs :",[439,440,441,444,447],"ol",{},[402,442,443],{},"une seule source de vérité pour le contrat HTTP ;",[402,445,446],{},"empêcher la compilation du client si le contrat avec le serveur n'est pas respecté ;",[402,448,449],{},"une solution compatible avec une codebase déjà existante, pas seulement avec du nouveau code.",[391,451,453],{"id":452},"les-solutions-envisagées","Les solutions envisagées",[455,456,458],"h3",{"id":457},"définir-un-contrat-openapi","Définir un contrat OpenAPI",[338,460,461],{},"La première option envisagée était de formaliser un contrat OpenAPI pour l'ensemble de l'API, puis\nde générer les types clients à partir de cette spec via de la codegen.",[338,463,464],{},"C'est une approche valide dans beaucoup de cas et elle fonctionne particulièrement bien quand\nplusieurs équipes ou partenaires consomment une API publique, ou quand la documentation est le\nproduit central.",[338,466,467],{},"Mais dans notre cas, cette option est lourde. Elle demande de modifier beaucoup de routes pour\nproduire un contrat OpenAPI propre et homogène. Elle ajoute aussi une couche de maintenance\nsupplémentaire : il faut s'assurer que la spec reste alignée avec les schémas Fastify et les\nhandlers existants.",[338,469,470],{},"Notre besoin est plus ciblé : tirer parti des contrats déjà présents dans le serveur.",[455,472,474],{"id":473},"créer-un-package-de-types-partagé","Créer un package de types partagé",[338,476,477],{},"La deuxième option envisagée était de créer un package de types partagé entre le client et le\nserveur.",[338,479,480],{},"Cette option paraît simple, mais elle a deux problèmes :",[439,482,483,486],{},[402,484,485],{},"Elle demande également de repasser sur beaucoup de routes existantes pour extraire et déplacer\nles types.",[402,487,488],{},"Elle impose de maintenir une structure de dossiers parallèle à l'API dans un autre package.",[338,490,491],{},"Avec le temps, ce package risque de devenir difficile à organiser :",[399,493,494,497,500],{},[402,495,496],{},"Quels types sont encore utilisés ?",[402,498,499],{},"Qui en est propriétaire ?",[402,501,502],{},"Comment éviter les types trop génériques ou les doublons ?",[338,504,505,506],{},"→ ",[351,507,508],{},"On veut éviter de créer une nouvelle zone grise entre le serveur et les clients.",[455,510,512],{"id":511},"générer-les-types-depuis-les-schémas-existants","Générer les types depuis les schémas existants",[338,514,515],{},"La troisième option consistait à générer les types à partir des JSON Schemas déjà présents sur les\nroutes.",[338,517,518],{},"Cette option colle mieux à notre codebase. Les schémas existent déjà. Ils sont déjà attachés aux\nroutes. Ils couvrent aussi certains routeurs legacy en JavaScript, ce qui est important pour assurer\nune migration progressive.",[338,520,521],{},"Il restait encore deux questions techniques :",[399,523,524],{},[402,525,526],{},[527,528,529],"em",{},"Existe-t-il une librairie capable de transformer un JSON Schema en type TypeScript ?",[338,531,532,533,544],{},"Bonne nouvelle,\n",[534,535,541],"a",{"href":536,"target":537,"rel":538},"https:\u002F\u002Fnpmx.dev\u002Fpackage\u002Fjson-schema-to-typescript","_blank",[539,540],"noopener","noreferrer",[358,542,543],{},"json-schema-to-typescript","\npermet de compiler un JSON Schema en définition TypeScript !",[399,546,547],{},[402,548,549],{},[527,550,551],{},"Comment collecter toutes les routes du serveur pour générer un registre exploitable côté client\n?",[338,553,554,555,558],{},"C'est ce qui allait devenir le cœur de notre implémentation : créer un ",[351,556,557],{},"plugin Fastify"," qui\ncollecte les routes API, extrait leurs schémas, génère les types, puis les expose dans un format\nconsommable par un client typé.",[391,560,562],{"id":561},"le-choix-retenu-on-génère-uniquement-du-typage","Le choix retenu : on génère uniquement du typage",[338,564,565],{},"Le compromis choisi est le suivant :",[399,567,568,571,574],{},[402,569,570],{},"le serveur reste la seule source de vérité ;",[402,572,573],{},"on ne génère que des types TypeScript, pas de runtime ;",[402,575,576],{},"les clients gardent leur couche HTTP existante, on branche juste le typage dessus.",[338,578,579],{},"Ce registre permet par exemple d'obtenir les informations suivantes :",[399,581,582,593,604,607],{},[402,583,584,585,588,589,592],{},"pour la méthode ",[358,586,587],{},"POST",", l'URL ",[358,590,591],{},"\u002Fusers\u002F:userId\u002Fmessages"," existe-t-elle ?",[402,594,595,596,599,600,603],{},"quels ",[358,597,598],{},"pathParams"," sont attendus ? (",[358,601,602],{},":userId"," )",[402,605,606],{},"quel body est autorisé ?",[402,608,609],{},"quel est le format de la réponse ?",[391,611,613],{"id":612},"architecture-de-la-solution","Architecture de la solution",[338,615,616],{},"Vue d'ensemble du pipeline, du serveur Fastify jusqu'aux clients typés :",[618,619,623],"pre",{"className":620,"code":621,"language":622,"meta":11,"style":11},"language-mermaid shiki shiki-themes github-light github-dark","flowchart TB\n    subgraph server[Serveur Fastify]\n        R[Routes + JSON Schema] --> H1[Hook onRoute\u003Cbr\u002F>collecte méthode, URL, schémas]\n        H1 --> H2[Hook onReady\u003Cbr\u002F>déclenche la génération]\n    end\n\n    H2 --> GEN[Générateur\u003Cbr\u002F>package `json-schema-to-typescript`]\n\n    subgraph out[Types générés .d.ts]\n        F1[un fichier par route] --> IDX[index.d.ts\u003Cbr\u002F>ApiTypes]\n    end\n\n    GEN --> F1\n\n    IDX --> W[Wrapper HTTP typé\u003Cbr\u002F>api.get \u002F api.post ...]\n\n    subgraph clients[Clients]\n        W --> WEB[Client web]\n        W --> MOB[Application mobile]\n    end\n\n    classDef highlight fill:#FFE8A3,stroke:#B7791F,stroke-width:2px,color:#1A202C;\n    class GEN highlight;\n","mermaid",[358,624,625,633,638,644,650,656,662,668,673,679,685,690,695,701,706,712,717,723,729,735,740,745,751],{"__ignoreMap":11},[626,627,629],"span",{"class":628,"line":19},"line",[626,630,632],{"class":631},"sVt8B","flowchart TB\n",[626,634,635],{"class":628,"line":12},[626,636,637],{"class":631},"    subgraph server[Serveur Fastify]\n",[626,639,641],{"class":628,"line":640},3,[626,642,643],{"class":631},"        R[Routes + JSON Schema] --> H1[Hook onRoute\u003Cbr\u002F>collecte méthode, URL, schémas]\n",[626,645,647],{"class":628,"line":646},4,[626,648,649],{"class":631},"        H1 --> H2[Hook onReady\u003Cbr\u002F>déclenche la génération]\n",[626,651,653],{"class":628,"line":652},5,[626,654,655],{"class":631},"    end\n",[626,657,659],{"class":628,"line":658},6,[626,660,661],{"emptyLinePlaceholder":17},"\n",[626,663,665],{"class":628,"line":664},7,[626,666,667],{"class":631},"    H2 --> GEN[Générateur\u003Cbr\u002F>package `json-schema-to-typescript`]\n",[626,669,671],{"class":628,"line":670},8,[626,672,661],{"emptyLinePlaceholder":17},[626,674,676],{"class":628,"line":675},9,[626,677,678],{"class":631},"    subgraph out[Types générés .d.ts]\n",[626,680,682],{"class":628,"line":681},10,[626,683,684],{"class":631},"        F1[un fichier par route] --> IDX[index.d.ts\u003Cbr\u002F>ApiTypes]\n",[626,686,688],{"class":628,"line":687},11,[626,689,655],{"class":631},[626,691,693],{"class":628,"line":692},12,[626,694,661],{"emptyLinePlaceholder":17},[626,696,698],{"class":628,"line":697},13,[626,699,700],{"class":631},"    GEN --> F1\n",[626,702,704],{"class":628,"line":703},14,[626,705,661],{"emptyLinePlaceholder":17},[626,707,709],{"class":628,"line":708},15,[626,710,711],{"class":631},"    IDX --> W[Wrapper HTTP typé\u003Cbr\u002F>api.get \u002F api.post ...]\n",[626,713,715],{"class":628,"line":714},16,[626,716,661],{"emptyLinePlaceholder":17},[626,718,720],{"class":628,"line":719},17,[626,721,722],{"class":631},"    subgraph clients[Clients]\n",[626,724,726],{"class":628,"line":725},18,[626,727,728],{"class":631},"        W --> WEB[Client web]\n",[626,730,732],{"class":628,"line":731},19,[626,733,734],{"class":631},"        W --> MOB[Application mobile]\n",[626,736,738],{"class":628,"line":737},20,[626,739,655],{"class":631},[626,741,743],{"class":628,"line":742},21,[626,744,661],{"emptyLinePlaceholder":17},[626,746,748],{"class":628,"line":747},22,[626,749,750],{"class":631},"    classDef highlight fill:#FFE8A3,stroke:#B7791F,stroke-width:2px,color:#1A202C;\n",[626,752,754],{"class":628,"line":753},23,[626,755,756],{"class":631},"    class GEN highlight;\n",[455,758,760],{"id":759},"_1-collecter-les-routes-api","1. Collecter les routes API",[338,762,763,764,767],{},"Le générateur est branché sous forme de plugin Fastify, on écoute le hook ",[358,765,766],{},"onRoute",", qui est appelé\nà chaque fois qu'une nouvelle route est enregistrée. À ce moment-là on collecte la méthode HTTP,\nl'URL, et les schémas Fastify. On collecte aussi quelques métadonnées comme le fichier source dans\nlequel la route est définie.",[338,769,770],{},"En pseudo-code ça donne :",[618,772,776],{"className":773,"code":774,"language":775,"meta":11,"style":11},"language-ts shiki shiki-themes github-light github-dark","const collectedRoutes = [];\n\nfastify.addHook(\"onRoute\", (routeOptions) => {\n  collectedRoutes.push({\n    method: routeOptions.method,\n    url: routeOptions.url,\n    schema: routeOptions.schema,\n  });\n});\n","ts",[358,777,778,794,798,830,841,846,851,856,861],{"__ignoreMap":11},[626,779,780,784,788,791],{"class":628,"line":19},[626,781,783],{"class":782},"szBVR","const",[626,785,787],{"class":786},"sj4cs"," collectedRoutes",[626,789,790],{"class":782}," =",[626,792,793],{"class":631}," [];\n",[626,795,796],{"class":628,"line":12},[626,797,661],{"emptyLinePlaceholder":17},[626,799,800,803,807,810,814,817,821,824,827],{"class":628,"line":640},[626,801,802],{"class":631},"fastify.",[626,804,806],{"class":805},"sScJk","addHook",[626,808,809],{"class":631},"(",[626,811,813],{"class":812},"sZZnC","\"onRoute\"",[626,815,816],{"class":631},", (",[626,818,820],{"class":819},"s4XuR","routeOptions",[626,822,823],{"class":631},") ",[626,825,826],{"class":782},"=>",[626,828,829],{"class":631}," {\n",[626,831,832,835,838],{"class":628,"line":646},[626,833,834],{"class":631},"  collectedRoutes.",[626,836,837],{"class":805},"push",[626,839,840],{"class":631},"({\n",[626,842,843],{"class":628,"line":652},[626,844,845],{"class":631},"    method: routeOptions.method,\n",[626,847,848],{"class":628,"line":658},[626,849,850],{"class":631},"    url: routeOptions.url,\n",[626,852,853],{"class":628,"line":664},[626,854,855],{"class":631},"    schema: routeOptions.schema,\n",[626,857,858],{"class":628,"line":670},[626,859,860],{"class":631},"  });\n",[626,862,863],{"class":628,"line":675},[626,864,865],{"class":631},"});\n",[338,867,868,869,871],{},"Cette approche est volontairement runtime (on ne fait pas d'analyse statique).",[345,870],{},"\nOn démarre l'application, on laisse Fastify enregistrer ses routes puis on observe le résultat.",[338,873,874],{},"Cela évite plusieurs pièges :",[399,876,877,880,883,890],{},[402,878,879],{},"une route définie mais jamais branchée ne sera pas générée ;",[402,881,882],{},"une route ajoutée par un plugin sera collectée ;",[402,884,885,886,889],{},"les préfixes Fastify appliqués lors du ",[358,887,888],{},"register"," sont pris en compte ;",[402,891,892],{},"le générateur n'a pas besoin de comprendre l'organisation interne du code.",[455,894,896],{"id":895},"_2-lancer-la-génération-quand-lapplication-est-prête","2. Lancer la génération quand l'application est prête",[338,898,899,900,361],{},"Une fois toutes les routes enregistrées, Fastify déclenche le hook ",[358,901,902],{},"onReady",[338,904,905],{},"C'est à ce moment-là que le générateur transforme la liste collectée en fichiers TypeScript.",[338,907,908],{},"Pseudo-code simplifié :",[618,910,912],{"className":773,"code":911,"language":775,"meta":11,"style":11},"fastify.addHook(\"onReady\", async () => {\n  await generateTypes(collectedRoutes);\n});\n",[358,913,914,938,949],{"__ignoreMap":11},[626,915,916,918,920,922,925,928,931,934,936],{"class":628,"line":19},[626,917,802],{"class":631},[626,919,806],{"class":805},[626,921,809],{"class":631},[626,923,924],{"class":812},"\"onReady\"",[626,926,927],{"class":631},", ",[626,929,930],{"class":782},"async",[626,932,933],{"class":631}," () ",[626,935,826],{"class":782},[626,937,829],{"class":631},[626,939,940,943,946],{"class":628,"line":12},[626,941,942],{"class":782},"  await",[626,944,945],{"class":805}," generateTypes",[626,947,948],{"class":631},"(collectedRoutes);\n",[626,950,951],{"class":628,"line":640},[626,952,865],{"class":631},[338,954,955],{},"Ce choix garantit que l'on génère les types à partir de la liste complète des routes, pas à partir\nd'un état intermédiaire du serveur.",[455,957,959],{"id":958},"_3-convertir-les-json-schemas-en-typescript","3. Convertir les JSON Schemas en TypeScript",[338,961,962],{},"Pour chaque route, le générateur extrait les schémas utiles :",[399,964,965,973,981,988],{},[402,966,967,969,970,972],{},[358,968,367],{}," devient ",[358,971,598],{}," côté client ;",[402,974,975,969,977,980],{},[358,976,371],{},[358,978,979],{},"query"," ;",[402,982,983,985,986,980],{},[358,984,375],{}," reste ",[358,987,375],{},[402,989,990,993],{},[358,991,992],{},"response"," devient le type de retour.",[338,995,996],{},"Puis chaque JSON Schema est converti en TypeScript.",[338,998,999],{},"Une route comme celle-ci :",[618,1001,1003],{"className":773,"code":1002,"language":775,"meta":11,"style":11},"const messagesRoutes = async (fastify) => {\n  fastify.post(\n    \"\u002F:userId\u002Fmessages\",\n    {\n      schema: {\n        params: {\n          type: \"object\",\n          required: [\"userId\"],\n          additionalProperties: false,\n          properties: {\n            userId: { type: \"string\" },\n          },\n        },\n        body: {\n          type: \"object\",\n          required: [\"content\"],\n          additionalProperties: false,\n          properties: {\n            content: { type: \"string\" },\n          },\n        },\n        response: {\n          201: {\n            type: \"object\",\n            required: [\"id\", \"content\"],\n            additionalProperties: false,\n            properties: {\n              id: { type: \"string\" },\n              content: { type: \"string\" },\n            },\n          },\n        },\n      },\n    },\n    async (request, reply) => {\n      \u002F\u002F implémentation de la route...\n    },\n  );\n};\n\nfastify.register(messagesRoutes, { prefix: \"\u002Fusers\" });\n",[358,1004,1005,1029,1040,1048,1053,1058,1063,1073,1084,1094,1099,1110,1115,1120,1125,1133,1142,1150,1154,1163,1167,1171,1176,1184,1194,1209,1219,1225,1235,1245,1251,1256,1261,1267,1273,1295,1302,1307,1313,1319,1324],{"__ignoreMap":11},[626,1006,1007,1009,1012,1014,1017,1020,1023,1025,1027],{"class":628,"line":19},[626,1008,783],{"class":782},[626,1010,1011],{"class":805}," messagesRoutes",[626,1013,790],{"class":782},[626,1015,1016],{"class":782}," async",[626,1018,1019],{"class":631}," (",[626,1021,1022],{"class":819},"fastify",[626,1024,823],{"class":631},[626,1026,826],{"class":782},[626,1028,829],{"class":631},[626,1030,1031,1034,1037],{"class":628,"line":12},[626,1032,1033],{"class":631},"  fastify.",[626,1035,1036],{"class":805},"post",[626,1038,1039],{"class":631},"(\n",[626,1041,1042,1045],{"class":628,"line":640},[626,1043,1044],{"class":812},"    \"\u002F:userId\u002Fmessages\"",[626,1046,1047],{"class":631},",\n",[626,1049,1050],{"class":628,"line":646},[626,1051,1052],{"class":631},"    {\n",[626,1054,1055],{"class":628,"line":652},[626,1056,1057],{"class":631},"      schema: {\n",[626,1059,1060],{"class":628,"line":658},[626,1061,1062],{"class":631},"        params: {\n",[626,1064,1065,1068,1071],{"class":628,"line":664},[626,1066,1067],{"class":631},"          type: ",[626,1069,1070],{"class":812},"\"object\"",[626,1072,1047],{"class":631},[626,1074,1075,1078,1081],{"class":628,"line":670},[626,1076,1077],{"class":631},"          required: [",[626,1079,1080],{"class":812},"\"userId\"",[626,1082,1083],{"class":631},"],\n",[626,1085,1086,1089,1092],{"class":628,"line":675},[626,1087,1088],{"class":631},"          additionalProperties: ",[626,1090,1091],{"class":786},"false",[626,1093,1047],{"class":631},[626,1095,1096],{"class":628,"line":681},[626,1097,1098],{"class":631},"          properties: {\n",[626,1100,1101,1104,1107],{"class":628,"line":687},[626,1102,1103],{"class":631},"            userId: { type: ",[626,1105,1106],{"class":812},"\"string\"",[626,1108,1109],{"class":631}," },\n",[626,1111,1112],{"class":628,"line":692},[626,1113,1114],{"class":631},"          },\n",[626,1116,1117],{"class":628,"line":697},[626,1118,1119],{"class":631},"        },\n",[626,1121,1122],{"class":628,"line":703},[626,1123,1124],{"class":631},"        body: {\n",[626,1126,1127,1129,1131],{"class":628,"line":708},[626,1128,1067],{"class":631},[626,1130,1070],{"class":812},[626,1132,1047],{"class":631},[626,1134,1135,1137,1140],{"class":628,"line":714},[626,1136,1077],{"class":631},[626,1138,1139],{"class":812},"\"content\"",[626,1141,1083],{"class":631},[626,1143,1144,1146,1148],{"class":628,"line":719},[626,1145,1088],{"class":631},[626,1147,1091],{"class":786},[626,1149,1047],{"class":631},[626,1151,1152],{"class":628,"line":725},[626,1153,1098],{"class":631},[626,1155,1156,1159,1161],{"class":628,"line":731},[626,1157,1158],{"class":631},"            content: { type: ",[626,1160,1106],{"class":812},[626,1162,1109],{"class":631},[626,1164,1165],{"class":628,"line":737},[626,1166,1114],{"class":631},[626,1168,1169],{"class":628,"line":742},[626,1170,1119],{"class":631},[626,1172,1173],{"class":628,"line":747},[626,1174,1175],{"class":631},"        response: {\n",[626,1177,1178,1181],{"class":628,"line":753},[626,1179,1180],{"class":786},"          201",[626,1182,1183],{"class":631},": {\n",[626,1185,1187,1190,1192],{"class":628,"line":1186},24,[626,1188,1189],{"class":631},"            type: ",[626,1191,1070],{"class":812},[626,1193,1047],{"class":631},[626,1195,1197,1200,1203,1205,1207],{"class":628,"line":1196},25,[626,1198,1199],{"class":631},"            required: [",[626,1201,1202],{"class":812},"\"id\"",[626,1204,927],{"class":631},[626,1206,1139],{"class":812},[626,1208,1083],{"class":631},[626,1210,1212,1215,1217],{"class":628,"line":1211},26,[626,1213,1214],{"class":631},"            additionalProperties: ",[626,1216,1091],{"class":786},[626,1218,1047],{"class":631},[626,1220,1222],{"class":628,"line":1221},27,[626,1223,1224],{"class":631},"            properties: {\n",[626,1226,1228,1231,1233],{"class":628,"line":1227},28,[626,1229,1230],{"class":631},"              id: { type: ",[626,1232,1106],{"class":812},[626,1234,1109],{"class":631},[626,1236,1238,1241,1243],{"class":628,"line":1237},29,[626,1239,1240],{"class":631},"              content: { type: ",[626,1242,1106],{"class":812},[626,1244,1109],{"class":631},[626,1246,1248],{"class":628,"line":1247},30,[626,1249,1250],{"class":631},"            },\n",[626,1252,1254],{"class":628,"line":1253},31,[626,1255,1114],{"class":631},[626,1257,1259],{"class":628,"line":1258},32,[626,1260,1119],{"class":631},[626,1262,1264],{"class":628,"line":1263},33,[626,1265,1266],{"class":631},"      },\n",[626,1268,1270],{"class":628,"line":1269},34,[626,1271,1272],{"class":631},"    },\n",[626,1274,1276,1279,1281,1284,1286,1289,1291,1293],{"class":628,"line":1275},35,[626,1277,1278],{"class":782},"    async",[626,1280,1019],{"class":631},[626,1282,1283],{"class":819},"request",[626,1285,927],{"class":631},[626,1287,1288],{"class":819},"reply",[626,1290,823],{"class":631},[626,1292,826],{"class":782},[626,1294,829],{"class":631},[626,1296,1298],{"class":628,"line":1297},36,[626,1299,1301],{"class":1300},"sJ8bj","      \u002F\u002F implémentation de la route...\n",[626,1303,1305],{"class":628,"line":1304},37,[626,1306,1272],{"class":631},[626,1308,1310],{"class":628,"line":1309},38,[626,1311,1312],{"class":631},"  );\n",[626,1314,1316],{"class":628,"line":1315},39,[626,1317,1318],{"class":631},"};\n",[626,1320,1322],{"class":628,"line":1321},40,[626,1323,661],{"emptyLinePlaceholder":17},[626,1325,1327,1329,1331,1334,1337],{"class":628,"line":1326},41,[626,1328,802],{"class":631},[626,1330,888],{"class":805},[626,1332,1333],{"class":631},"(messagesRoutes, { prefix: ",[626,1335,1336],{"class":812},"\"\u002Fusers\"",[626,1338,1339],{"class":631}," });\n",[338,1341,1342],{},"devient un type de route :",[618,1344,1346],{"className":773,"code":1345,"language":775,"meta":11,"style":11},"\u002F\u002F users_userId_messages.POST.ts\n\ntype Route = {\n  input: {\n    pathParams: {\n      userId: string;\n    };\n    body: {\n      content: string;\n    };\n  };\n  response: {\n    201: {\n      id: string;\n      content: string;\n    };\n  };\n};\n",[358,1347,1348,1353,1357,1369,1379,1388,1401,1406,1415,1426,1430,1435,1444,1453,1464,1474,1478,1482],{"__ignoreMap":11},[626,1349,1350],{"class":628,"line":19},[626,1351,1352],{"class":1300},"\u002F\u002F users_userId_messages.POST.ts\n",[626,1354,1355],{"class":628,"line":12},[626,1356,661],{"emptyLinePlaceholder":17},[626,1358,1359,1362,1365,1367],{"class":628,"line":640},[626,1360,1361],{"class":782},"type",[626,1363,1364],{"class":805}," Route",[626,1366,790],{"class":782},[626,1368,829],{"class":631},[626,1370,1371,1374,1377],{"class":628,"line":646},[626,1372,1373],{"class":819},"  input",[626,1375,1376],{"class":782},":",[626,1378,829],{"class":631},[626,1380,1381,1384,1386],{"class":628,"line":652},[626,1382,1383],{"class":819},"    pathParams",[626,1385,1376],{"class":782},[626,1387,829],{"class":631},[626,1389,1390,1393,1395,1398],{"class":628,"line":658},[626,1391,1392],{"class":819},"      userId",[626,1394,1376],{"class":782},[626,1396,1397],{"class":786}," string",[626,1399,1400],{"class":631},";\n",[626,1402,1403],{"class":628,"line":664},[626,1404,1405],{"class":631},"    };\n",[626,1407,1408,1411,1413],{"class":628,"line":670},[626,1409,1410],{"class":819},"    body",[626,1412,1376],{"class":782},[626,1414,829],{"class":631},[626,1416,1417,1420,1422,1424],{"class":628,"line":675},[626,1418,1419],{"class":819},"      content",[626,1421,1376],{"class":782},[626,1423,1397],{"class":786},[626,1425,1400],{"class":631},[626,1427,1428],{"class":628,"line":681},[626,1429,1405],{"class":631},[626,1431,1432],{"class":628,"line":687},[626,1433,1434],{"class":631},"  };\n",[626,1436,1437,1440,1442],{"class":628,"line":692},[626,1438,1439],{"class":819},"  response",[626,1441,1376],{"class":782},[626,1443,829],{"class":631},[626,1445,1446,1449,1451],{"class":628,"line":697},[626,1447,1448],{"class":786},"    201",[626,1450,1376],{"class":782},[626,1452,829],{"class":631},[626,1454,1455,1458,1460,1462],{"class":628,"line":703},[626,1456,1457],{"class":819},"      id",[626,1459,1376],{"class":782},[626,1461,1397],{"class":786},[626,1463,1400],{"class":631},[626,1465,1466,1468,1470,1472],{"class":628,"line":708},[626,1467,1419],{"class":819},[626,1469,1376],{"class":782},[626,1471,1397],{"class":786},[626,1473,1400],{"class":631},[626,1475,1476],{"class":628,"line":714},[626,1477,1405],{"class":631},[626,1479,1480],{"class":628,"line":719},[626,1481,1434],{"class":631},[626,1483,1484],{"class":628,"line":725},[626,1485,1318],{"class":631},[455,1487,1489],{"id":1488},"_4-générer-lindex-global-des-routes","4. Générer l'index global des routes",[338,1491,1492],{},"Chaque route est générée dans un fichier dédié. Ensuite, un fichier d'index agrège toutes les routes\npar méthode HTTP.",[338,1494,1495],{},"Schématiquement :",[618,1497,1499],{"className":773,"code":1498,"language":775,"meta":11,"style":11},"\u002F\u002F index.d.ts\n\nexport type ApiRoutesPost = {\n  \"\u002Fusers\u002F:userId\u002Fmessages\": import(\".\u002Fusers_userId_messages.POST\").default;\n  \"\u002Fprojects\u002F:projectId\u002Finvitations\": import(\".\u002Fprojects_projectId_invitations.POST\").default;\n};\n\nexport type ApiRoutesGet = {\n  \"\u002Fusers\u002Fme\": import(\".\u002Fusers_me.GET\").default;\n  \"\u002Fprojects\u002F:projectId\": import(\".\u002Fprojects_projectId.GET\").default;\n};\n\nexport type ApiTypes = {\n  GET: ApiRoutesGet;\n  POST: ApiRoutesPost;\n};\n",[358,1500,1501,1506,1510,1525,1548,1568,1572,1576,1589,1609,1629,1633,1637,1650,1661,1672],{"__ignoreMap":11},[626,1502,1503],{"class":628,"line":19},[626,1504,1505],{"class":1300},"\u002F\u002F index.d.ts\n",[626,1507,1508],{"class":628,"line":12},[626,1509,661],{"emptyLinePlaceholder":17},[626,1511,1512,1515,1518,1521,1523],{"class":628,"line":640},[626,1513,1514],{"class":782},"export",[626,1516,1517],{"class":782}," type",[626,1519,1520],{"class":805}," ApiRoutesPost",[626,1522,790],{"class":782},[626,1524,829],{"class":631},[626,1526,1527,1530,1532,1535,1537,1540,1543,1546],{"class":628,"line":646},[626,1528,1529],{"class":812},"  \"\u002Fusers\u002F:userId\u002Fmessages\"",[626,1531,1376],{"class":782},[626,1533,1534],{"class":782}," import",[626,1536,809],{"class":631},[626,1538,1539],{"class":812},"\".\u002Fusers_userId_messages.POST\"",[626,1541,1542],{"class":631},").",[626,1544,1545],{"class":805},"default",[626,1547,1400],{"class":631},[626,1549,1550,1553,1555,1557,1559,1562,1564,1566],{"class":628,"line":652},[626,1551,1552],{"class":812},"  \"\u002Fprojects\u002F:projectId\u002Finvitations\"",[626,1554,1376],{"class":782},[626,1556,1534],{"class":782},[626,1558,809],{"class":631},[626,1560,1561],{"class":812},"\".\u002Fprojects_projectId_invitations.POST\"",[626,1563,1542],{"class":631},[626,1565,1545],{"class":805},[626,1567,1400],{"class":631},[626,1569,1570],{"class":628,"line":658},[626,1571,1318],{"class":631},[626,1573,1574],{"class":628,"line":664},[626,1575,661],{"emptyLinePlaceholder":17},[626,1577,1578,1580,1582,1585,1587],{"class":628,"line":670},[626,1579,1514],{"class":782},[626,1581,1517],{"class":782},[626,1583,1584],{"class":805}," ApiRoutesGet",[626,1586,790],{"class":782},[626,1588,829],{"class":631},[626,1590,1591,1594,1596,1598,1600,1603,1605,1607],{"class":628,"line":675},[626,1592,1593],{"class":812},"  \"\u002Fusers\u002Fme\"",[626,1595,1376],{"class":782},[626,1597,1534],{"class":782},[626,1599,809],{"class":631},[626,1601,1602],{"class":812},"\".\u002Fusers_me.GET\"",[626,1604,1542],{"class":631},[626,1606,1545],{"class":805},[626,1608,1400],{"class":631},[626,1610,1611,1614,1616,1618,1620,1623,1625,1627],{"class":628,"line":681},[626,1612,1613],{"class":812},"  \"\u002Fprojects\u002F:projectId\"",[626,1615,1376],{"class":782},[626,1617,1534],{"class":782},[626,1619,809],{"class":631},[626,1621,1622],{"class":812},"\".\u002Fprojects_projectId.GET\"",[626,1624,1542],{"class":631},[626,1626,1545],{"class":805},[626,1628,1400],{"class":631},[626,1630,1631],{"class":628,"line":687},[626,1632,1318],{"class":631},[626,1634,1635],{"class":628,"line":692},[626,1636,661],{"emptyLinePlaceholder":17},[626,1638,1639,1641,1643,1646,1648],{"class":628,"line":697},[626,1640,1514],{"class":782},[626,1642,1517],{"class":782},[626,1644,1645],{"class":805}," ApiTypes",[626,1647,790],{"class":782},[626,1649,829],{"class":631},[626,1651,1652,1655,1657,1659],{"class":628,"line":703},[626,1653,1654],{"class":819},"  GET",[626,1656,1376],{"class":782},[626,1658,1584],{"class":805},[626,1660,1400],{"class":631},[626,1662,1663,1666,1668,1670],{"class":628,"line":708},[626,1664,1665],{"class":819},"  POST",[626,1667,1376],{"class":782},[626,1669,1520],{"class":805},[626,1671,1400],{"class":631},[626,1673,1674],{"class":628,"line":714},[626,1675,1318],{"class":631},[338,1677,1678],{},"Au final on obtient un dossier avec la structure suivante :",[618,1680,1685],{"className":1681,"code":1683,"language":1684},[1682],"language-text","generated-server-types\u002F\n├── index.d.ts\n├── users_userId_messages.POST.ts\n├── projects_projectId_invitations.POST.ts\n├── users_me.GET.ts\n└── projects_projectId.GET.ts\n└── ... \u002F\u002F autres routes\n","text",[358,1686,1683],{"__ignoreMap":11},[391,1688,1690],{"id":1689},"consommer-les-types-côté-client","Consommer les types côté client",[338,1692,1693,1694,1697,1698,1701],{},"Les applications clientes n'importent pas les fichiers de routes un par un. Elles importent le type\nglobal ",[358,1695,1696],{},"ApiTypes"," défini dans ",[358,1699,1700],{},"index.d.ts",", puis l'utilisent pour typer leur client HTTP.",[338,1703,1704],{},"L'idée est d'écrire un wrapper générique autour du client HTTP existant. Ce wrapper contient un peu\nde code runtime, puisqu'il doit réellement envoyer la requête, remplacer les path params et\ntransmettre le body ou la query string. En revanche, toute la connaissance des routes doit rester au\nniveau des types.",[338,1706,1707],{},"Voici un exemple partiel du code qui permet de typer le client :",[618,1709,1711],{"className":773,"code":1710,"language":775,"meta":11,"style":11},"\u002F\u002F extrait les inputs d'une route, seulement si elle existe\ntype RouteInputs\u003CRoute> = Route extends { input: infer Input } ? Input : never;\n\n\u002F\u002F extraire la réponse d'une route, seulement si elle existe\ntype RouteResponse\u003CRoute> = Route extends { response: infer Response }\n  ? Response[keyof Response]\n  : unknown;\n\n\u002F\u002F le fetcher est scopé par méthode HTTP\nfunction createFetcher\u003CMethod extends keyof ApiTypes>(method: Method) {\n  return async \u003CPath extends keyof ApiTypes[Method], Route extends ApiTypes[Method][Path]>(\n    path: Path,\n    ...args: RouteInputs\u003CRoute> extends never ? [] : [RouteInputs\u003CRoute>]\n  ): Promise\u003CRouteResponse\u003CRoute>> => {\n    \u002F\u002F Ici, on appelle le client HTTP existant.\n    \u002F\u002F TypeScript, lui, a déjà validé path, args et response.\n    return httpClient.request({ method, path, args });\n  };\n}\n",[358,1712,1713,1718,1772,1776,1781,1814,1832,1842,1846,1851,1885,1929,1941,1984,2010,2015,2020,2033,2037],{"__ignoreMap":11},[626,1714,1715],{"class":628,"line":19},[626,1716,1717],{"class":1300},"\u002F\u002F extrait les inputs d'une route, seulement si elle existe\n",[626,1719,1720,1722,1725,1728,1731,1734,1737,1739,1742,1745,1748,1750,1753,1756,1759,1762,1764,1767,1770],{"class":628,"line":12},[626,1721,1361],{"class":782},[626,1723,1724],{"class":805}," RouteInputs",[626,1726,1727],{"class":631},"\u003C",[626,1729,1730],{"class":805},"Route",[626,1732,1733],{"class":631},"> ",[626,1735,1736],{"class":782},"=",[626,1738,1364],{"class":805},[626,1740,1741],{"class":782}," extends",[626,1743,1744],{"class":631}," { ",[626,1746,1747],{"class":819},"input",[626,1749,1376],{"class":782},[626,1751,1752],{"class":782}," infer",[626,1754,1755],{"class":805}," Input",[626,1757,1758],{"class":631}," } ",[626,1760,1761],{"class":782},"?",[626,1763,1755],{"class":805},[626,1765,1766],{"class":782}," :",[626,1768,1769],{"class":786}," never",[626,1771,1400],{"class":631},[626,1773,1774],{"class":628,"line":640},[626,1775,661],{"emptyLinePlaceholder":17},[626,1777,1778],{"class":628,"line":646},[626,1779,1780],{"class":1300},"\u002F\u002F extraire la réponse d'une route, seulement si elle existe\n",[626,1782,1783,1785,1788,1790,1792,1794,1796,1798,1800,1802,1804,1806,1808,1811],{"class":628,"line":652},[626,1784,1361],{"class":782},[626,1786,1787],{"class":805}," RouteResponse",[626,1789,1727],{"class":631},[626,1791,1730],{"class":805},[626,1793,1733],{"class":631},[626,1795,1736],{"class":782},[626,1797,1364],{"class":805},[626,1799,1741],{"class":782},[626,1801,1744],{"class":631},[626,1803,992],{"class":819},[626,1805,1376],{"class":782},[626,1807,1752],{"class":782},[626,1809,1810],{"class":805}," Response",[626,1812,1813],{"class":631}," }\n",[626,1815,1816,1819,1821,1824,1827,1829],{"class":628,"line":658},[626,1817,1818],{"class":782},"  ?",[626,1820,1810],{"class":805},[626,1822,1823],{"class":631},"[",[626,1825,1826],{"class":782},"keyof",[626,1828,1810],{"class":805},[626,1830,1831],{"class":631},"]\n",[626,1833,1834,1837,1840],{"class":628,"line":664},[626,1835,1836],{"class":782},"  :",[626,1838,1839],{"class":786}," unknown",[626,1841,1400],{"class":631},[626,1843,1844],{"class":628,"line":670},[626,1845,661],{"emptyLinePlaceholder":17},[626,1847,1848],{"class":628,"line":675},[626,1849,1850],{"class":1300},"\u002F\u002F le fetcher est scopé par méthode HTTP\n",[626,1852,1853,1856,1859,1861,1864,1866,1869,1871,1874,1877,1879,1882],{"class":628,"line":681},[626,1854,1855],{"class":782},"function",[626,1857,1858],{"class":805}," createFetcher",[626,1860,1727],{"class":631},[626,1862,1863],{"class":805},"Method",[626,1865,1741],{"class":782},[626,1867,1868],{"class":782}," keyof",[626,1870,1645],{"class":805},[626,1872,1873],{"class":631},">(",[626,1875,1876],{"class":819},"method",[626,1878,1376],{"class":782},[626,1880,1881],{"class":805}," Method",[626,1883,1884],{"class":631},") {\n",[626,1886,1887,1890,1892,1895,1898,1900,1902,1904,1906,1908,1911,1913,1915,1917,1919,1921,1924,1926],{"class":628,"line":687},[626,1888,1889],{"class":782},"  return",[626,1891,1016],{"class":782},[626,1893,1894],{"class":631}," \u003C",[626,1896,1897],{"class":805},"Path",[626,1899,1741],{"class":782},[626,1901,1868],{"class":782},[626,1903,1645],{"class":805},[626,1905,1823],{"class":631},[626,1907,1863],{"class":805},[626,1909,1910],{"class":631},"], ",[626,1912,1730],{"class":805},[626,1914,1741],{"class":782},[626,1916,1645],{"class":805},[626,1918,1823],{"class":631},[626,1920,1863],{"class":805},[626,1922,1923],{"class":631},"][",[626,1925,1897],{"class":805},[626,1927,1928],{"class":631},"]>(\n",[626,1930,1931,1934,1936,1939],{"class":628,"line":692},[626,1932,1933],{"class":819},"    path",[626,1935,1376],{"class":782},[626,1937,1938],{"class":805}," Path",[626,1940,1047],{"class":631},[626,1942,1943,1946,1949,1951,1953,1955,1957,1959,1962,1964,1966,1969,1971,1974,1977,1979,1981],{"class":628,"line":697},[626,1944,1945],{"class":782},"    ...",[626,1947,1948],{"class":819},"args",[626,1950,1376],{"class":782},[626,1952,1724],{"class":805},[626,1954,1727],{"class":631},[626,1956,1730],{"class":805},[626,1958,1733],{"class":631},[626,1960,1961],{"class":782},"extends",[626,1963,1769],{"class":786},[626,1965,383],{"class":782},[626,1967,1968],{"class":631}," [] ",[626,1970,1376],{"class":782},[626,1972,1973],{"class":631}," [",[626,1975,1976],{"class":805},"RouteInputs",[626,1978,1727],{"class":631},[626,1980,1730],{"class":805},[626,1982,1983],{"class":631},">]\n",[626,1985,1986,1989,1991,1994,1996,1999,2001,2003,2006,2008],{"class":628,"line":703},[626,1987,1988],{"class":631},"  )",[626,1990,1376],{"class":782},[626,1992,1993],{"class":805}," Promise",[626,1995,1727],{"class":631},[626,1997,1998],{"class":805},"RouteResponse",[626,2000,1727],{"class":631},[626,2002,1730],{"class":805},[626,2004,2005],{"class":631},">> ",[626,2007,826],{"class":782},[626,2009,829],{"class":631},[626,2011,2012],{"class":628,"line":708},[626,2013,2014],{"class":1300},"    \u002F\u002F Ici, on appelle le client HTTP existant.\n",[626,2016,2017],{"class":628,"line":714},[626,2018,2019],{"class":1300},"    \u002F\u002F TypeScript, lui, a déjà validé path, args et response.\n",[626,2021,2022,2025,2028,2030],{"class":628,"line":719},[626,2023,2024],{"class":782},"    return",[626,2026,2027],{"class":631}," httpClient.",[626,2029,1283],{"class":805},[626,2031,2032],{"class":631},"({ method, path, args });\n",[626,2034,2035],{"class":628,"line":725},[626,2036,1434],{"class":631},[626,2038,2039],{"class":628,"line":731},[626,2040,2041],{"class":631},"}\n",[338,2043,2044],{},"À l'aide du wrapper, on expose une API facile d'utilisation avec une fonction par méthode HTTP\nsupportée :",[618,2046,2048],{"className":773,"code":2047,"language":775,"meta":11,"style":11},"export const api = {\n  get: createFetcher(\"GET\"),\n  post: createFetcher(\"POST\"),\n  put: createFetcher(\"PUT\"),\n  patch: createFetcher(\"PATCH\"),\n  delete: createFetcher(\"DELETE\"),\n};\n",[358,2049,2050,2064,2080,2094,2108,2122,2136],{"__ignoreMap":11},[626,2051,2052,2054,2057,2060,2062],{"class":628,"line":19},[626,2053,1514],{"class":782},[626,2055,2056],{"class":782}," const",[626,2058,2059],{"class":786}," api",[626,2061,790],{"class":782},[626,2063,829],{"class":631},[626,2065,2066,2069,2072,2074,2077],{"class":628,"line":12},[626,2067,2068],{"class":631},"  get: ",[626,2070,2071],{"class":805},"createFetcher",[626,2073,809],{"class":631},[626,2075,2076],{"class":812},"\"GET\"",[626,2078,2079],{"class":631},"),\n",[626,2081,2082,2085,2087,2089,2092],{"class":628,"line":640},[626,2083,2084],{"class":631},"  post: ",[626,2086,2071],{"class":805},[626,2088,809],{"class":631},[626,2090,2091],{"class":812},"\"POST\"",[626,2093,2079],{"class":631},[626,2095,2096,2099,2101,2103,2106],{"class":628,"line":646},[626,2097,2098],{"class":631},"  put: ",[626,2100,2071],{"class":805},[626,2102,809],{"class":631},[626,2104,2105],{"class":812},"\"PUT\"",[626,2107,2079],{"class":631},[626,2109,2110,2113,2115,2117,2120],{"class":628,"line":652},[626,2111,2112],{"class":631},"  patch: ",[626,2114,2071],{"class":805},[626,2116,809],{"class":631},[626,2118,2119],{"class":812},"\"PATCH\"",[626,2121,2079],{"class":631},[626,2123,2124,2127,2129,2131,2134],{"class":628,"line":658},[626,2125,2126],{"class":631},"  delete: ",[626,2128,2071],{"class":805},[626,2130,809],{"class":631},[626,2132,2133],{"class":812},"\"DELETE\"",[626,2135,2079],{"class":631},[626,2137,2138],{"class":628,"line":664},[626,2139,1318],{"class":631},[338,2141,2142],{},"L'appel client devient alors :",[618,2144,2146],{"className":773,"code":2145,"language":775,"meta":11,"style":11},"const message = await api.post(\"\u002Fusers\u002F:userId\u002Fmessages\", {\n  pathParams: { userId: \"user_123\" },\n  body: { content: \"Hello\" },\n});\n",[358,2147,2148,2173,2183,2193],{"__ignoreMap":11},[626,2149,2150,2152,2155,2157,2160,2163,2165,2167,2170],{"class":628,"line":19},[626,2151,783],{"class":782},[626,2153,2154],{"class":786}," message",[626,2156,790],{"class":782},[626,2158,2159],{"class":782}," await",[626,2161,2162],{"class":631}," api.",[626,2164,1036],{"class":805},[626,2166,809],{"class":631},[626,2168,2169],{"class":812},"\"\u002Fusers\u002F:userId\u002Fmessages\"",[626,2171,2172],{"class":631},", {\n",[626,2174,2175,2178,2181],{"class":628,"line":12},[626,2176,2177],{"class":631},"  pathParams: { userId: ",[626,2179,2180],{"class":812},"\"user_123\"",[626,2182,1109],{"class":631},[626,2184,2185,2188,2191],{"class":628,"line":640},[626,2186,2187],{"class":631},"  body: { content: ",[626,2189,2190],{"class":812},"\"Hello\"",[626,2192,1109],{"class":631},[626,2194,2195],{"class":628,"line":646},[626,2196,865],{"class":631},[338,2198,2199],{},"À ce stade, TypeScript sait que :",[399,2201,2202,2211,2217,2222],{},[402,2203,2204,2207,2208,2210],{},[358,2205,2206],{},"'\u002Fusers\u002F:userId\u002Fmessages'"," est une route ",[358,2209,587],{}," valide ;",[402,2212,2213,2216],{},[358,2214,2215],{},"pathParams.userId"," est obligatoire ;",[402,2218,2219,2216],{},[358,2220,2221],{},"body.content",[402,2223,2224,2227,2228,2231],{},[358,2225,2226],{},"message.id"," et ",[358,2229,2230],{},"message.content"," existent dans la réponse.",[338,2233,2234,2235,2237,2238,2240],{},"Si on oublie ",[358,2236,598],{}," ou si le ",[358,2239,375],{}," est incompatible, le code ne compilera pas.",[385,2242,2243],{},[338,2244,2245],{},"Le client HTTP reste le même. Ce qui change, c'est que TypeScript connaît maintenant le contrat de\nchaque endpoint.",[338,2247,2248],{},"Au moment de la compilation, les types générés disparaissent. Le JavaScript final ne contient que le\nwrapper HTTP réellement utilisé par l'application.",[385,2250,2251],{},[338,2252,2253],{},"Le but était de ne pas faire grossir le bundle du client.",[391,2255,2257],{"id":2256},"dériver-des-types-métier-depuis-les-routes","Dériver des types métier depuis les routes",[338,2259,2260],{},"Un autre bénéfice important est la possibilité de dériver des types depuis le contrat API.",[338,2262,2263],{},"Par exemple :",[618,2265,2267],{"className":773,"code":2266,"language":775,"meta":11,"style":11},"type CreateMessageBody = ExtractBody\u003C\"POST\", \"\u002Fusers\u002F:userId\u002Fmessages\">;\n\ntype CreateMessageResponse = ExtractResponse\u003C\"POST\", \"\u002Fusers\u002F:userId\u002Fmessages\">;\n\ntype Message = CreateMessageResponse;\n",[358,2268,2269,2292,2296,2318,2322],{"__ignoreMap":11},[626,2270,2271,2273,2276,2278,2281,2283,2285,2287,2289],{"class":628,"line":19},[626,2272,1361],{"class":782},[626,2274,2275],{"class":805}," CreateMessageBody",[626,2277,790],{"class":782},[626,2279,2280],{"class":805}," ExtractBody",[626,2282,1727],{"class":631},[626,2284,2091],{"class":812},[626,2286,927],{"class":631},[626,2288,2169],{"class":812},[626,2290,2291],{"class":631},">;\n",[626,2293,2294],{"class":628,"line":12},[626,2295,661],{"emptyLinePlaceholder":17},[626,2297,2298,2300,2303,2305,2308,2310,2312,2314,2316],{"class":628,"line":640},[626,2299,1361],{"class":782},[626,2301,2302],{"class":805}," CreateMessageResponse",[626,2304,790],{"class":782},[626,2306,2307],{"class":805}," ExtractResponse",[626,2309,1727],{"class":631},[626,2311,2091],{"class":812},[626,2313,927],{"class":631},[626,2315,2169],{"class":812},[626,2317,2291],{"class":631},[626,2319,2320],{"class":628,"line":646},[626,2321,661],{"emptyLinePlaceholder":17},[626,2323,2324,2326,2329,2331,2333],{"class":628,"line":652},[626,2325,1361],{"class":782},[626,2327,2328],{"class":805}," Message",[626,2330,790],{"class":782},[626,2332,2302],{"class":805},[626,2334,1400],{"class":631},[338,2336,2337],{},"Ces helpers évitent de recréer des DTO côté client.",[338,2339,2340],{},"Ils sont particulièrement utiles dans les formulaires, les stores, les hooks de data fetching ou les\nmodèles de présentation.",[338,2342,2343],{},"Exemple :",[618,2345,2347],{"className":773,"code":2346,"language":775,"meta":11,"style":11},"type UpdateProfileBody = ExtractBody\u003C\"PUT\", \"\u002Fusers\u002Fme\u002Fprofile\">;\n\nfunction buildUpdateProfileBody(form: ProfileForm): UpdateProfileBody {\n  return {\n    firstName: form.firstName,\n    lastName: form.lastName,\n  };\n}\n",[358,2348,2349,2371,2375,2401,2407,2412,2417,2421],{"__ignoreMap":11},[626,2350,2351,2353,2356,2358,2360,2362,2364,2366,2369],{"class":628,"line":19},[626,2352,1361],{"class":782},[626,2354,2355],{"class":805}," UpdateProfileBody",[626,2357,790],{"class":782},[626,2359,2280],{"class":805},[626,2361,1727],{"class":631},[626,2363,2105],{"class":812},[626,2365,927],{"class":631},[626,2367,2368],{"class":812},"\"\u002Fusers\u002Fme\u002Fprofile\"",[626,2370,2291],{"class":631},[626,2372,2373],{"class":628,"line":12},[626,2374,661],{"emptyLinePlaceholder":17},[626,2376,2377,2379,2382,2384,2387,2389,2392,2395,2397,2399],{"class":628,"line":640},[626,2378,1855],{"class":782},[626,2380,2381],{"class":805}," buildUpdateProfileBody",[626,2383,809],{"class":631},[626,2385,2386],{"class":819},"form",[626,2388,1376],{"class":782},[626,2390,2391],{"class":805}," ProfileForm",[626,2393,2394],{"class":631},")",[626,2396,1376],{"class":782},[626,2398,2355],{"class":805},[626,2400,829],{"class":631},[626,2402,2403,2405],{"class":628,"line":646},[626,2404,1889],{"class":782},[626,2406,829],{"class":631},[626,2408,2409],{"class":628,"line":652},[626,2410,2411],{"class":631},"    firstName: form.firstName,\n",[626,2413,2414],{"class":628,"line":658},[626,2415,2416],{"class":631},"    lastName: form.lastName,\n",[626,2418,2419],{"class":628,"line":664},[626,2420,1434],{"class":631},[626,2422,2423],{"class":628,"line":670},[626,2424,2041],{"class":631},[338,2426,2427],{},"Si le backend rend un champ obligatoire, ou change le nom d'une propriété, le type dérivé change\nautomatiquement. Le code client qui construit le body est alors vérifié au typecheck.",[391,2429,2431],{"id":2430},"pourquoi-ne-pas-parser-statiquement-le-code","Pourquoi ne pas parser statiquement le code ?",[338,2433,2434,2435,361],{},"On aurait pu faire le choix d'analyser statiquement les fichiers de routes pour trouver les appels à\n",[358,2436,2437],{},"fastify.route",[338,2439,2440],{},"Mais cette approche devient vite fragile.",[338,2442,2443],{},"Dans une application réelle, les routes ne sont pas toujours déclarées au même endroit. Elles\npeuvent être regroupées dans des plugins, préfixées dynamiquement, enregistrées conditionnellement,\ncomposées par module, ou encapsulées dans des helpers.",[338,2445,2446],{},"Un analyseur statique devrait comprendre beaucoup de conventions internes pour reconstruire la\nsurface HTTP réelle.",[338,2448,2449,2450,2452],{},"Fastify, lui, connaît déjà cette surface. Le hook ",[358,2451,766],{}," fournit donc un point d'observation\nbeaucoup plus fiable.",[338,2454,2455],{},"Le compromis est clair :",[399,2457,2458,2461,2464],{},[402,2459,2460],{},"la génération runtime est plus fidèle à l'application réellement démarrée ;",[402,2462,2463],{},"elle est moins dépendante de l'organisation du code ;",[402,2465,2466],{},"elle coûte plus cher, parce qu'il faut booter le serveur.",[338,2468,2469],{},"Dans notre cas, ce coût est acceptable pour une commande de génération lancée en local ou en CI.",[391,2471,2473],{"id":2472},"intégration-dans-le-workflow-de-développement","Intégration dans le workflow de développement",[338,2475,2476],{},"La génération est exposée via une commande dédiée :",[618,2478,2482],{"className":2479,"code":2480,"language":2481,"meta":11,"style":11},"language-bash shiki shiki-themes github-light github-dark","pnpm gen:client-types\n","bash",[358,2483,2484],{"__ignoreMap":11},[626,2485,2486,2489],{"class":628,"line":19},[626,2487,2488],{"class":805},"pnpm",[626,2490,2491],{"class":812}," gen:client-types\n",[338,2493,2494,2495,2498],{},"Cette commande démarre le serveur dans un environnement prévu pour la génération, collecte les\nroutes, puis écrit les fichiers ",[358,2496,2497],{},".d.ts"," dans un package partagé.",[338,2500,2501],{},"Les fichiers générés sont commités.",[338,2503,2504],{},"Ce choix peut faire débat. Beaucoup d'équipes préfèrent ne pas commiter les fichiers générés. Dans\nnotre cas, les commiter a plusieurs avantages :",[399,2506,2507,2510,2513,2516],{},[402,2508,2509],{},"les clients peuvent consommer les types sans lancer le générateur ni devoir build le serveur ;",[402,2511,2512],{},"les changements de contrat sont visibles en revue de code ;",[402,2514,2515],{},"la CI peut détecter qu'une PR a modifié des routes sans mettre à jour les types (on a d'ailleurs\nune action CI pour les générer automatiquement) ;",[402,2517,2518],{},"les packages clients n'ont pas besoin de connaître les détails du serveur pour typechecker.",[338,2520,2521],{},"Si les types générés ne sont pas à jour, la CI échoue ou signale explicitement qu'il faut relancer\nla génération.",[391,2523,2525],{"id":2524},"ce-que-cette-approche-change-au-quotidien","Ce que cette approche change au quotidien",[338,2527,2528],{},"Le changement le plus visible est dans l'expérience de développement au quotidien.",[338,2530,2531],{},"Avant, écrire un appel API consistait souvent à connaître l'URL, lire le backend, trouver ou recréer\nle type du body, puis espérer que la réponse était correctement modélisée.",[338,2533,2534],{},"Après génération, l'URL devient elle-même une clé typée.",[618,2536,2538],{"className":773,"code":2537,"language":775,"meta":11,"style":11},"\u002F\u002F l'autocomplétion propose uniquement les URLs existantes en GET\nawait api.get(\"\u002Fusers\u002Fme\");\n",[358,2539,2540,2545],{"__ignoreMap":11},[626,2541,2542],{"class":628,"line":19},[626,2543,2544],{"class":1300},"\u002F\u002F l'autocomplétion propose uniquement les URLs existantes en GET\n",[626,2546,2547,2550,2552,2555,2557,2560],{"class":628,"line":12},[626,2548,2549],{"class":782},"await",[626,2551,2162],{"class":631},[626,2553,2554],{"class":805},"get",[626,2556,809],{"class":631},[626,2558,2559],{"class":812},"\"\u002Fusers\u002Fme\"",[626,2561,2562],{"class":631},");\n",[338,2564,2565],{},"Si la route existe, TypeScript infère la réponse.",[618,2567,2569],{"className":773,"code":2568,"language":775,"meta":11,"style":11},"const user = await api.get(\"\u002Fusers\u002Fme\");\n\nuser.email; \u002F\u002F ✅ string, typé depuis le schéma de réponse\nuser.subscription.plan; \u002F\u002F ✅ inféré, autocomplété\nuser.emial; \u002F\u002F ❌ Property 'emial' does not exist on type ...\n",[358,2570,2571,2592,2596,2604,2612],{"__ignoreMap":11},[626,2572,2573,2575,2578,2580,2582,2584,2586,2588,2590],{"class":628,"line":19},[626,2574,783],{"class":782},[626,2576,2577],{"class":786}," user",[626,2579,790],{"class":782},[626,2581,2159],{"class":782},[626,2583,2162],{"class":631},[626,2585,2554],{"class":805},[626,2587,809],{"class":631},[626,2589,2559],{"class":812},[626,2591,2562],{"class":631},[626,2593,2594],{"class":628,"line":12},[626,2595,661],{"emptyLinePlaceholder":17},[626,2597,2598,2601],{"class":628,"line":640},[626,2599,2600],{"class":631},"user.email; ",[626,2602,2603],{"class":1300},"\u002F\u002F ✅ string, typé depuis le schéma de réponse\n",[626,2605,2606,2609],{"class":628,"line":646},[626,2607,2608],{"class":631},"user.subscription.plan; ",[626,2610,2611],{"class":1300},"\u002F\u002F ✅ inféré, autocomplété\n",[626,2613,2614,2617],{"class":628,"line":652},[626,2615,2616],{"class":631},"user.emial; ",[626,2618,2619],{"class":1300},"\u002F\u002F ❌ Property 'emial' does not exist on type ...\n",[338,2621,2622],{},"Si la route n'existe pas pour cette méthode, TypeScript le signale.",[618,2624,2626],{"className":773,"code":2625,"language":775,"meta":11,"style":11},"\u002F\u002F ❌ Argument of type '\"\u002Fusers\u002Fme\"' is not assignable to\n\u002F\u002F    parameter of type keyof ApiRoutesPost\nawait api.post(\"\u002Fusers\u002Fme\");\n",[358,2627,2628,2633,2638],{"__ignoreMap":11},[626,2629,2630],{"class":628,"line":19},[626,2631,2632],{"class":1300},"\u002F\u002F ❌ Argument of type '\"\u002Fusers\u002Fme\"' is not assignable to\n",[626,2634,2635],{"class":628,"line":12},[626,2636,2637],{"class":1300},"\u002F\u002F    parameter of type keyof ApiRoutesPost\n",[626,2639,2640,2642,2644,2646,2648,2650],{"class":628,"line":640},[626,2641,2549],{"class":782},[626,2643,2162],{"class":631},[626,2645,1036],{"class":805},[626,2647,809],{"class":631},[626,2649,2559],{"class":812},[626,2651,2562],{"class":631},[338,2653,2654],{},"Pour les routes avec paramètres, le client est guidé :",[618,2656,2658],{"className":773,"code":2657,"language":775,"meta":11,"style":11},"await api.get(\"\u002Fprojects\u002F:projectId\", {\n  pathParams: {\n    projectId: \"project_123\", \u002F\u002F ✅ requis : l'oublier déclenche une erreur\n  },\n});\n",[358,2659,2660,2675,2680,2693,2698],{"__ignoreMap":11},[626,2661,2662,2664,2666,2668,2670,2673],{"class":628,"line":19},[626,2663,2549],{"class":782},[626,2665,2162],{"class":631},[626,2667,2554],{"class":805},[626,2669,809],{"class":631},[626,2671,2672],{"class":812},"\"\u002Fprojects\u002F:projectId\"",[626,2674,2172],{"class":631},[626,2676,2677],{"class":628,"line":12},[626,2678,2679],{"class":631},"  pathParams: {\n",[626,2681,2682,2685,2688,2690],{"class":628,"line":640},[626,2683,2684],{"class":631},"    projectId: ",[626,2686,2687],{"class":812},"\"project_123\"",[626,2689,927],{"class":631},[626,2691,2692],{"class":1300},"\u002F\u002F ✅ requis : l'oublier déclenche une erreur\n",[626,2694,2695],{"class":628,"line":646},[626,2696,2697],{"class":631},"  },\n",[626,2699,2700],{"class":628,"line":652},[626,2701,865],{"class":631},[338,2703,2704],{},"Pour les mutations, le body est contraint :",[618,2706,2708],{"className":773,"code":2707,"language":775,"meta":11,"style":11},"await api.put(\"\u002Fusers\u002Fme\u002Fprofile\", {\n  body: {\n    firstName: \"Ada\",\n    lastName: \"Lovelace\",\n    \u002F\u002F ❌ une propriété inconnue ou un type incorrect est rejeté ici\n  },\n});\n",[358,2709,2710,2725,2730,2740,2750,2755,2759],{"__ignoreMap":11},[626,2711,2712,2714,2716,2719,2721,2723],{"class":628,"line":19},[626,2713,2549],{"class":782},[626,2715,2162],{"class":631},[626,2717,2718],{"class":805},"put",[626,2720,809],{"class":631},[626,2722,2368],{"class":812},[626,2724,2172],{"class":631},[626,2726,2727],{"class":628,"line":12},[626,2728,2729],{"class":631},"  body: {\n",[626,2731,2732,2735,2738],{"class":628,"line":640},[626,2733,2734],{"class":631},"    firstName: ",[626,2736,2737],{"class":812},"\"Ada\"",[626,2739,1047],{"class":631},[626,2741,2742,2745,2748],{"class":628,"line":646},[626,2743,2744],{"class":631},"    lastName: ",[626,2746,2747],{"class":812},"\"Lovelace\"",[626,2749,1047],{"class":631},[626,2751,2752],{"class":628,"line":652},[626,2753,2754],{"class":1300},"    \u002F\u002F ❌ une propriété inconnue ou un type incorrect est rejeté ici\n",[626,2756,2757],{"class":628,"line":658},[626,2758,2697],{"class":631},[626,2760,2761],{"class":628,"line":664},[626,2762,865],{"class":631},[338,2764,2765],{},"Cela ne supprime pas tous les bugs, mais cela élimine une catégorie entière d'incohérences entre\nclient et serveur.",[391,2767,2769],{"id":2768},"les-bénéfices","Les bénéfices",[399,2771,2772,2778,2784,2790],{},[402,2773,2774,2777],{},[351,2775,2776],{},"Moins de duplication"," : les DTO ne sont plus recopiés dans chaque client, le type est dérivé du\nschéma serveur. Les types de présentation propres à l'UI restent, eux, côté client.",[402,2779,2780,2783],{},[351,2781,2782],{},"Des refactorings plus sûrs"," : quand une route change (paramètre renommé, champ rendu\nobligatoire, réponse modifiée, route supprimée…), les clients incompatibles cassent au typecheck.",[402,2785,2786,2789],{},[351,2787,2788],{},"Une adoption progressive"," : pas de SDK complet à migrer d'un bloc. Un client garde sa couche\nHTTP et adopte le wrapper typé module par module — décisif dans une codebase existante.",[402,2791,2792,2795],{},[351,2793,2794],{},"Une meilleure revue de contrat"," : quand une PR modifie un schéma, le diff des types générés\nmontre l'impact pour les consommateurs.",[391,2797,2799],{"id":2798},"les-limites","Les limites",[399,2801,2802,2812,2834,2840,2846],{},[402,2803,2804,2807,2808,2811],{},[351,2805,2806],{},"La qualité dépend des schémas"," : le générateur n'invente rien. Un schéma absent, trop large ou\nen ",[358,2809,2810],{},"unknown"," produit un type d'autant moins utile. L'approche incite à mieux maintenir les\nschémas, sans remplacer cette discipline.",[402,2813,2814,2817,2818,927,2821,927,2824,927,2827,927,2830,2833],{},[351,2815,2816],{},"Les réponses d'erreur restent difficiles"," : notre première version type surtout les succès. Les\nerreurs (",[358,2819,2820],{},"400",[358,2822,2823],{},"401",[358,2825,2826],{},"404",[358,2828,2829],{},"409",[358,2831,2832],{},"422","…) font partie du contrat mais soulèvent des questions\nde format commun, de déclaration par route et d'ergonomie côté client.",[402,2835,2836,2839],{},[351,2837,2838],{},"Les fichiers générés ajoutent du bruit"," : un fichier par route est simple à générer mais\nproduit des diffs volumineux quand les schémas bougent souvent.",[402,2841,2842,2845],{},[351,2843,2844],{},"La génération runtime a un coût"," : démarrer le serveur est plus fidèle mais plus lent, et\nsuppose que l'environnement enregistre bien toutes les routes (flags, modules optionnels,\nconfiguration).",[402,2847,2848,2851],{},[351,2849,2850],{},"TypeScript ne valide pas le runtime"," : si le serveur renvoie une donnée hors schéma, le client\nne le verra pas à l'exécution. Il faudrait pour cela une validation côté client ou serveur.",[391,2853,2855],{"id":2854},"améliorations","Améliorations",[338,2857,2858],{},"Plusieurs pistes sont déjà identifiées :",[399,2860,2861,2867,2876,2882,2888],{},[402,2862,2863,2866],{},[351,2864,2865],{},"Mieux typer les erreurs",", pour gérer proprement conflits, validations ou ressources absentes ;",[402,2868,2869,2872,2873,2875],{},[351,2870,2871],{},"Produire un rapport de qualité des schémas"," (routes sans réponse, types tombant en ",[358,2874,2810],{},",\nstatuts ignorés…) pour en faire un outil d'amélioration du contrat ;",[402,2877,2878,2881],{},[351,2879,2880],{},"Réduire le volume généré"," avec un format plus compact, meilleur pour les diffs et le typecheck\n;",[402,2883,2884,2887],{},[351,2885,2886],{},"Générer plus d'outillage client de façon optionnelle"," (helpers d'URL, query keys, remplacement\ndes path params…) sans enfermer les clients ;",[402,2889,2890,2893],{},[351,2891,2892],{},"Articuler types statiques et validation runtime",", utile là où client et serveur peuvent se\ndésynchroniser, notamment sur mobile.",[391,2895,2897],{"id":2896},"en-conclusion","En conclusion",[338,2899,2900],{},"La solution à mettre en place dépendra du contexte de votre projet. Si vous démarrez un nouveau\nprojet full TypeScript, tRPC ou une architecture RPC typée sera sûrement plus directe. Si vous\nexposez une API publique consommée par des partenaires, OpenAPI-first sera sans doute plus adapté.\nEt si vous cherchez à standardiser fortement tous les appels réseau, un SDK généré aura plus de\nsens.",[338,2902,2903,2904,2910],{},"Chez ",[534,2905,2909],{"href":2906,"rel":2907},"\u002F\u002Fwww.indy.fr",[2908],"nofollow","Indy",", nous partions d'un serveur Fastify, de routes déjà décrites par JSON\nSchema et de plusieurs clients à connecter. Générer les types depuis cet existant était donc la\nsolution la plus simple à implémenter, et celle qui offrait le meilleur rapport impact\u002Feffort.",[2912,2913,2914],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":11,"searchDepth":12,"depth":12,"links":2916},[2917,2918,2923,2924,2930,2931,2932,2933,2934,2935,2936,2937,2938],{"id":393,"depth":12,"text":394},{"id":452,"depth":12,"text":453,"children":2919},[2920,2921,2922],{"id":457,"depth":640,"text":458},{"id":473,"depth":640,"text":474},{"id":511,"depth":640,"text":512},{"id":561,"depth":12,"text":562},{"id":612,"depth":12,"text":613,"children":2925},[2926,2927,2928,2929],{"id":759,"depth":640,"text":760},{"id":895,"depth":640,"text":896},{"id":958,"depth":640,"text":959},{"id":1488,"depth":640,"text":1489},{"id":1689,"depth":12,"text":1690},{"id":2256,"depth":12,"text":2257},{"id":2430,"depth":12,"text":2431},{"id":2472,"depth":12,"text":2473},{"id":2524,"depth":12,"text":2525},{"id":2768,"depth":12,"text":2769},{"id":2798,"depth":12,"text":2799},{"id":2854,"depth":12,"text":2855},{"id":2896,"depth":12,"text":2897},"2026-06-09","Comment nous avons transformé nos schémas de routes backend en contrats TypeScript consommables par nos clients web et mobile, sans générer un SDK complet.","fr",{},"\u002Farticles\u002F2026-06-09-generation-de-types-depuis-routes-fastify",{"title":333,"description":2940},"articles\u002F2026-06-09-generation-de-types-depuis-routes-fastify",[2947],"Tech","CCXIckVBkmKvFcWEqt0NKTlUDMVHWGqd2Ro8GXhd04M",1782974626058]