[{"data":1,"prerenderedAt":5729},["ShallowReactive",2],{"post-\u002Ffr\u002Fposts\u002Fbuild-saas-apps-with-confidence-on-nutxhub-with-gavarnie":3,"post-link-fr-8c077ba8-63ca-4784-b693-0e1fa8623bf9":279},{"id":4,"title":5,"body":6,"contentId":263,"date":264,"description":265,"extension":266,"meta":267,"navigation":268,"path":269,"rawbody":270,"robots":271,"schemaOrg":271,"seo":272,"sitemap":273,"stem":277,"__hash__":278},"posts\u002Ffr\u002Fposts\u002Fbuild-saas-apps-with-confidence-on-nutxhub-with-gavarnie.md","Construire des Apps SaaS sur NuxtHub avec Gavarnie",{"type":7,"value":8,"toc":257},"minimark",[9,26,35,42,45,70,84,94,99,102,106,112,119,136,154,159,163,166,173,182,191,200,213,216],[10,11,12],"blockquote",{},[13,14,15,19,20,25],"p",{},[16,17,18],"span",{},"!NOTE","\nCe starter a été conçu pendant mon temps libre. Si vous le trouvez utile, pensez à ",[21,22,24],"a",{"href":23},"\u002Ffr\u002Fsponsorship","soutenir mon travail",".",[13,27,28,29],{},"TL;DR : ",[21,30,34],{"href":31,"rel":32},"https:\u002F\u002Fgithub.com\u002Fbarbapapazes\u002Fgavarnie",[33],"nofollow","Gavarnie",[13,36,37],{},[38,39],"img",{"alt":40,"src":41},"Image OpenGraph du projet Gavarnie","\u002Fposts\u002Fbuild-saas-apps-with-confidence-on-nutxhub-with-gavarnie\u002Fog.png",[13,43,44],{},"Avant d'explorer ce starter, permettez-moi de partager la motivation derrière sa création.",[13,46,47,48,53,54,59,60,65,66],{},"Il y a plusieurs mois, ",[21,49,52],{"href":50,"rel":51},"https:\u002F\u002Fgithub.com\u002Fatinux",[33],"Sébastien Chopin",", le créateur de Nuxt, a introduit ",[21,55,58],{"href":56,"rel":57},"https:\u002F\u002Fhub.nuxt.com",[33],"NuxtHub",", une plateforme pour déployer et gérer des applications Nuxt, alimentée par ",[21,61,64],{"href":62,"rel":63},"https:\u002F\u002Fcloudflare.com",[33],"Cloudflare",". En résumé, vous pouvez créer des applications en tirant parti de l'infrastructure de Cloudflare comme D1 (base de données) et R2 (stockage), permettant un déploiement et une gestion aisés. ",[67,68,69],"em",{},"C'est semblable à Vercel ou Netlify mais pour les applications Nuxt, et je la trouve exceptionnel.",[13,71,72,73,77,78,83],{},"Cependant, cela fonctionne sur ce qu'on appelle ",[74,75,76],"strong",{},"l'edge",", ce qui diffère des méthodes de déploiement d'applications traditionnelles comme VPS. L'edge utilise des workers, similaires aux web workers dans votre navigateur, mais côté serveur. Ce système permet un déploiement rapide et rentable, bien qu'il comporte plusieurs contraintes telles que les ",[21,79,82],{"href":80,"rel":81},"https:\u002F\u002Fdevelopers.cloudflare.com\u002Fworkers\u002Fruntime-apis\u002F",[33],"API disponibles"," et un temps de calcul limité. C'est un compromis.",[13,85,86,87,90,91],{},"L'edge introduit une nouvelle approche conceptuelle où les limitations nécessitent des solutions innovantes. ",[67,88,89],{},"Bien que des défis existent, soyez assuré que vous pouvez toujours atteindre vos résultats souhaités."," Ce nouvel environnement nécessite la construction d'un écosystème complet autour de lui. ",[67,92,93],{},"Ne vous attendez pas à des solutions prêtes à l'emploi pour tout ; j'élargirai ce point dans un autre article.",[10,95,96],{},[13,97,98],{},"Petit à petit, l'edge peut être être maîtrisé.",[13,100,101],{},"C'est ici que Gavarnie entre en jeu.",[103,104,34],"h2",{"id":105},"gavarnie",[13,107,108],{},[38,109],{"alt":110,"src":111},"Cirque de Gavarnie par Jean-Christophe BENOIST,\u003Cbr>CC BY 3.0 https:\u002F\u002Fcreativecommons.org\u002Flicenses\u002Fby\u002F3.0, via Wikimedia Commons","\u002Fposts\u002Fbuild-saas-apps-with-confidence-on-nutxhub-with-gavarnie\u002Fgavarnie.jpg",[13,113,114,115,118],{},"Gavarnie est un starter destiné à être utilisé avec NuxtHub, spécifiquement sur l'infrastructure Cloudflare. ",[67,116,117],{},"Si vous avez besoin de compatibilité avec Supabase, Turso ou Vercel, cela ne sera pas le bon choix."," Il propose une authentification sociale préconfigurée avec plusieurs fournisseurs, une vérification d'email, et met l'accent sur les meilleures pratiques architecturales et de sécurité.",[13,120,121,122,125,126,129,130,125,133,25],{},"Le plus important, c'est qu'il est ",[74,123,124],{},"gratuit"," et ",[74,127,128],{},"open source",", vous permettant de commencer votre projet SaaS dès maintenant ou de l'utiliser ",[74,131,132],{},"pour apprendre",[74,134,135],{},"favoriser le développement de l'écosystème",[13,137,138,139,142,143,146,147,150,151],{},"En effet, l'un de mes objectifs avec Gavarnie n'est pas de créer juste un autre starter, mais de faire avancer l'initiative pour promouvoir Nuxt et NuxtHub. Je suis convaincu qu'en établissant des bases communes et solides, nous pouvons collectivement renforcer l'écosystème autour de NuxtHub et de l'edge. Ainsi, Gavarnie sert de ",[74,140,141],{},"point de départ",", ",[74,144,145],{},"et non de fin",", et c'est à ",[74,148,149],{},"nous de nourrir sa croissance",". ",[67,152,153],{},"Je vous en prie, rejoignez-moi pour l'améliorer.",[10,155,156],{},[13,157,158],{},"Gavarnie est un début, pas une conclusion.",[103,160,162],{"id":161},"caractéristiques","Caractéristiques",[13,164,165],{},"Gavarnie est développé sur Nuxt et NuxtHub. Ces deux éléments constituent l'épine dorsale de ce projet, avec tous les éléments construits autour d'eux.",[13,167,168,169,172],{},"Sous sa forme actuelle, ",[21,170,34],{"href":31,"rel":171},[33]," permet aux utilisateurs de créer un compte en utilisant leurs profils sociaux (GitHub et Twitch), de lier des comptes sociaux, de changer leur adresse e-mail via un mécanisme de vérification, de mettre à jour leur profil et leur photo de profil, et de supprimer leur compte.",[13,174,175,176,181],{},"Le service d'email est facilité par ",[21,177,180],{"href":178,"rel":179},"https:\u002F\u002Fresend.io",[33],"Resend",", qui simplifie les transactions par email.",[13,183,184,185,190],{},"L'interface utilisateur est alimentée par ",[21,186,189],{"href":187,"rel":188},"https:\u002F\u002Fui.nuxt.com",[33],"Nuxt UI Pro",", un ensemble d'outils UI fourni par l'équipe Nuxt pour les applications Nuxt.",[13,192,193,194,199],{},"L'authentification sociale est proposée via le module ",[21,195,198],{"href":196,"rel":197},"https:\u002F\u002Fgithub.com\u002Fatinux\u002Fnuxt-auth-utils",[33],"nuxt-auth-utils",", qui simplifie l'authentification sociale dans Nuxt. Elle prend en charge plusieurs fournisseurs tels que Google, Facebook, X (Twitter), GitHub, entre autres.",[13,201,202,203,208,209,212],{},"De plus, j'ai incorporé le module ",[21,204,207],{"href":205,"rel":206},"https:\u002F\u002Fnuxt.com\u002Fmodules\u002Fsecurity",[33],"Nuxt Security"," pour améliorer la sécurité de l'application. Ce starter comprend une CSP préconfigurée, un CSRF ",[67,210,211],{},"que je prévois d'améliorer",", et un limiteur de débit pour prévenir les abus des points de terminaison sensibles.",[13,214,215],{},"Gavarnie est une continuité pour apporter des capacités full-stack aux applications Nuxt.",[217,218,220],"callout-read-more",{"id":219},"8c077ba8-63ca-4784-b693-0e1fa8623bf9",[217,221,223,231,235,242,248,254],{"id":222},"05da35d9-c21f-4b45-ba86-e88d61339502",[13,224,225,226],{},"Explorez le dépôt ",[227,228,230],"git-hub-link",{"repo":229},"barbapapazes\u002Fgavarnie"," pour découvrir plus sur ses fonctionnalités et comment les utiliser.",[103,232,234],{"id":233},"étape-par-étape","Étape par Étape",[13,236,237,238,241],{},"Ceci n'est que le ",[74,239,240],{},"début"," de l'aventure.",[13,243,244,245],{},"Gavarnie est un projet dynamique qui évoluera. Nous pouvons envisager d'incorporer une meilleure gestion des sessions, un tableau de bord administrateur, un système de jetons API, une solution de facturation, et plus encore. ",[67,246,247],{},"Les opportunités sont vastes, et c'est à nous d'impulser son développement.",[13,249,250,251],{},"Ayant pris connaissance de ",[227,252,253],{"repo":229},", n'hésitez pas à l'explorer, à l'étendre, à expérimenter et à créer des applications pratiques avec.",[13,255,256],{},"N'hésitez pas à donner une étoile, à engager une discussion sur le dépôt GitHub pour toute question ou suggestion visant à améliorer le starter ! 💛",{"title":258,"searchDepth":259,"depth":259,"links":260},"",2,[261,262],{"id":105,"depth":259,"text":34},{"id":161,"depth":259,"text":162},"a1efd078-cbb8-49b6-b557-258edfda5f59","2024-08-20","Construisez et déployez des applications SaaS sur NuxtHub avec Gavarnie. Il offre un login social, une vérification par email, il est gratuit et open-source !","md",{},true,"\u002Ffr\u002Fposts\u002Fbuild-saas-apps-with-confidence-on-nutxhub-with-gavarnie","---\ncontentId: a1efd078-cbb8-49b6-b557-258edfda5f59\ntitle: Construire des Apps SaaS sur NuxtHub avec Gavarnie\ndescription: Construisez et déployez des applications SaaS sur NuxtHub avec Gavarnie. Il offre un login social, une vérification par email, il est gratuit et open-source !\ndate: 2024-08-20\n---\n\n> [!NOTE]\n> Ce starter a été conçu pendant mon temps libre. Si vous le trouvez utile, pensez à [soutenir mon travail](\u002Ffr\u002Fsponsorship).\n\nTL;DR : [Gavarnie](https:\u002F\u002Fgithub.com\u002Fbarbapapazes\u002Fgavarnie)\n\n![Image OpenGraph du projet Gavarnie](\u002Fposts\u002Fbuild-saas-apps-with-confidence-on-nutxhub-with-gavarnie\u002Fog.png)\n\nAvant d'explorer ce starter, permettez-moi de partager la motivation derrière sa création.\n\nIl y a plusieurs mois, [Sébastien Chopin](https:\u002F\u002Fgithub.com\u002Fatinux), le créateur de Nuxt, a introduit [NuxtHub](https:\u002F\u002Fhub.nuxt.com), une plateforme pour déployer et gérer des applications Nuxt, alimentée par [Cloudflare](https:\u002F\u002Fcloudflare.com). En résumé, vous pouvez créer des applications en tirant parti de l'infrastructure de Cloudflare comme D1 (base de données) et R2 (stockage), permettant un déploiement et une gestion aisés. _C'est semblable à Vercel ou Netlify mais pour les applications Nuxt, et je la trouve exceptionnel._\n\nCependant, cela fonctionne sur ce qu'on appelle **l'edge**, ce qui diffère des méthodes de déploiement d'applications traditionnelles comme VPS. L'edge utilise des workers, similaires aux web workers dans votre navigateur, mais côté serveur. Ce système permet un déploiement rapide et rentable, bien qu'il comporte plusieurs contraintes telles que les [API disponibles](https:\u002F\u002Fdevelopers.cloudflare.com\u002Fworkers\u002Fruntime-apis\u002F) et un temps de calcul limité. C'est un compromis.\n\nL'edge introduit une nouvelle approche conceptuelle où les limitations nécessitent des solutions innovantes. _Bien que des défis existent, soyez assuré que vous pouvez toujours atteindre vos résultats souhaités._ Ce nouvel environnement nécessite la construction d'un écosystème complet autour de lui. _Ne vous attendez pas à des solutions prêtes à l'emploi pour tout ; j'élargirai ce point dans un autre article._\n\n> Petit à petit, l'edge peut être être maîtrisé.\n\nC'est ici que Gavarnie entre en jeu.\n\n## Gavarnie\n\n![Cirque de Gavarnie par Jean-Christophe BENOIST,\u003Cbr>CC BY 3.0 \u003Chttps:\u002F\u002Fcreativecommons.org\u002Flicenses\u002Fby\u002F3.0>, via Wikimedia Commons](\u002Fposts\u002Fbuild-saas-apps-with-confidence-on-nutxhub-with-gavarnie\u002Fgavarnie.jpg)\n\nGavarnie est un starter destiné à être utilisé avec NuxtHub, spécifiquement sur l'infrastructure Cloudflare. _Si vous avez besoin de compatibilité avec Supabase, Turso ou Vercel, cela ne sera pas le bon choix._ Il propose une authentification sociale préconfigurée avec plusieurs fournisseurs, une vérification d'email, et met l'accent sur les meilleures pratiques architecturales et de sécurité.\n\nLe plus important, c'est qu'il est **gratuit** et **open source**, vous permettant de commencer votre projet SaaS dès maintenant ou de l'utiliser **pour apprendre** et **favoriser le développement de l'écosystème**.\n\nEn effet, l'un de mes objectifs avec Gavarnie n'est pas de créer juste un autre starter, mais de faire avancer l'initiative pour promouvoir Nuxt et NuxtHub. Je suis convaincu qu'en établissant des bases communes et solides, nous pouvons collectivement renforcer l'écosystème autour de NuxtHub et de l'edge. Ainsi, Gavarnie sert de **point de départ**, **et non de fin**, et c'est à **nous de nourrir sa croissance**. _Je vous en prie, rejoignez-moi pour l'améliorer._\n\n> Gavarnie est un début, pas une conclusion.\n\n## Caractéristiques\n\nGavarnie est développé sur Nuxt et NuxtHub. Ces deux éléments constituent l'épine dorsale de ce projet, avec tous les éléments construits autour d'eux.\n\nSous sa forme actuelle, [Gavarnie](https:\u002F\u002Fgithub.com\u002Fbarbapapazes\u002Fgavarnie) permet aux utilisateurs de créer un compte en utilisant leurs profils sociaux (GitHub et Twitch), de lier des comptes sociaux, de changer leur adresse e-mail via un mécanisme de vérification, de mettre à jour leur profil et leur photo de profil, et de supprimer leur compte.\n\nLe service d'email est facilité par [Resend](https:\u002F\u002Fresend.io), qui simplifie les transactions par email.\n\nL'interface utilisateur est alimentée par [Nuxt UI Pro](https:\u002F\u002Fui.nuxt.com), un ensemble d'outils UI fourni par l'équipe Nuxt pour les applications Nuxt.\n\nL'authentification sociale est proposée via le module [nuxt-auth-utils](https:\u002F\u002Fgithub.com\u002Fatinux\u002Fnuxt-auth-utils), qui simplifie l'authentification sociale dans Nuxt. Elle prend en charge plusieurs fournisseurs tels que Google, Facebook, X (Twitter), GitHub, entre autres.\n\nDe plus, j'ai incorporé le module [Nuxt Security](https:\u002F\u002Fnuxt.com\u002Fmodules\u002Fsecurity) pour améliorer la sécurité de l'application. Ce starter comprend une CSP préconfigurée, un CSRF _que je prévois d'améliorer_, et un limiteur de débit pour prévenir les abus des points de terminaison sensibles.\n\nGavarnie est une continuité pour apporter des capacités full-stack aux applications Nuxt.\n\n\u003CCalloutReadMore id=\"8c077ba8-63ca-4784-b693-0e1fa8623bf9\" \u002F>\n\n\u003CCalloutReadMore id=\"05da35d9-c21f-4b45-ba86-e88d61339502\" \u002F>\n\nExplorez le dépôt \u003CGitHubLink repo=\"barbapapazes\u002Fgavarnie\" \u002F> pour découvrir plus sur ses fonctionnalités et comment les utiliser.\n\n## Étape par Étape\n\nCeci n'est que le **début** de l'aventure.\n\nGavarnie est un projet dynamique qui évoluera. Nous pouvons envisager d'incorporer une meilleure gestion des sessions, un tableau de bord administrateur, un système de jetons API, une solution de facturation, et plus encore. _Les opportunités sont vastes, et c'est à nous d'impulser son développement._\n\nAyant pris connaissance de \u003CGitHubLink repo=\"barbapapazes\u002Fgavarnie\" \u002F>, n'hésitez pas à l'explorer, à l'étendre, à expérimenter et à créer des applications pratiques avec.\n\nN'hésitez pas à donner une étoile, à engager une discussion sur le dépôt GitHub pour toute question ou suggestion visant à améliorer le starter ! 💛\n",null,{"title":5,"description":265},{"loc":269,"images":274},[275,276],{"loc":41},{"loc":111},"fr\u002Fposts\u002Fbuild-saas-apps-with-confidence-on-nutxhub-with-gavarnie","yA8CxYtcMTZ_KfBwTeVV9rSc5DUfLRcLFZFbSXNnxo8",{"id":280,"title":281,"body":282,"contentId":219,"date":5720,"description":5721,"extension":266,"meta":5722,"navigation":268,"path":5723,"rawbody":5724,"robots":271,"schemaOrg":271,"seo":5725,"sitemap":5726,"stem":5727,"__hash__":5728},"posts\u002Ffr\u002Fposts\u002Fnuxt-going-full-stack-how-to-streamline-form-validation.md","Nuxt Devient Full-Stack : Comment Valider des formulaires ?",{"type":7,"value":283,"toc":5714},[284,295,300,5710],[13,285,286,287,290,291,294],{},"Depuis des semaines, je construis une application full-stack appelée Orion avec Nuxt et ",[21,288,58],{"href":56,"rel":289},[33],". Tout au long de ce parcours, j'ai rencontré de nombreux défis, et l'un d'eux est la validation des formulaires. Dans cet article, je vais vous montrer comment j'ai réussi à ",[74,292,293],{},"valider les formulaires à la fois côté client et côté serveur sans duplications"," de la logique de validation, des champs de texte simples aux téléchargements de fichiers complexes avec NuxtHub.",[13,296,297],{},[67,298,299],{},"Orion est une collection de modèles basée sur la communauté pour votre prochain projet, des pages de destination aux applications web complètes. NuxtHub est une plateforme de déploiement et d'administration pour Nuxt, propulsée par Cloudflare.",[217,301,303,312,318,322,340,351,355,358,516,519,628,634,639,644,659,819,825],{"id":302},"2349bbcd-1535-4f33-8da7-299f46af18b9",[13,304,305],{},[67,306,307,308,311],{},"Pendant cet article, je vais parler de ",[21,309,58],{"href":56,"rel":310},[33]," car il fournit les primitives pour construire des applications full-stack, mais même si vous ne l'utilisez pas, vous pouvez facilement suivre cet article. Les formulaires seront gérés sans aucune bibliothèque.",[13,313,314,315],{},"Vous pouvez parcourir le code exemple dans ",[227,316,25],{"repo":317},"barbapapazes\u002Fvalidate-forms-in-nuxt",[103,319,321],{"id":320},"le-problème","Le Problème",[13,323,324,325,328,329,332,333,125,336,339],{},"Sur le web, les formulaires sont omniprésents. Ils sont le principal moyen d'interaction et de demande d'entrée utilisateur. Mais sur le web, il y a une règle essentielle : ",[74,326,327],{},"ne jamais faire confiance au client"," (absolument jamais et en aucun cas). Cela signifie que ",[74,330,331],{},"toutes les données"," provenant du client doivent être ",[74,334,335],{},"vérifiées",[74,337,338],{},"sanitisées"," sur le serveur avant d'être traitées ou stockées dans une base de données. Un client peut être un navigateur, une application mobile, une requête HTTP ou tout autre objet capable d'envoyer des données à votre serveur, construit par vous ou non.",[13,341,342,343,346,347,350],{},"Lorsque vous construisez une MPA traditionnelle (Multi-Page Application), seul le serveur est responsable de la validation. Pour une SPA (Single-Page Application), à la fois le client et le serveur sont responsables de la validation pour fournir une meilleure expérience utilisateur. ",[67,344,345],{},"Cela est simplifié car il existe un moyen de donner une impression de validation côté client avec une MPA traditionnelle, mais ce n'est pas le sujet de cet article."," MAIS, même si les données sont validées côté client, il est ",[74,348,349],{},"obligatoire"," de les valider côté serveur.",[103,352,354],{"id":353},"le-formulaire-le-plus-simple","Le Formulaire le Plus Simple",[13,356,357],{},"Commençons notre parcours avec le formulaire le plus simple : une seule entrée de texte.",[359,360,364],"pre",{"className":361,"code":362,"language":363,"meta":258,"style":258},"language-vue shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003Ctemplate>\n  \u003Cform method=\"post\" action=\"\u002Ftag\">\n    \u003Cinput type=\"text\" placeholder=\"Nom\" required>\n    \u003Cbutton type=\"submit\">\n      Créer Tag\n    \u003C\u002Fbutton>\n  \u003C\u002Fform>\n\u003C\u002Ftemplate>\n","vue",[365,366,367,382,420,458,479,486,496,506],"code",{"__ignoreMap":258},[16,368,371,375,379],{"class":369,"line":370},"line",1,[16,372,374],{"class":373},"sMK4o","\u003C",[16,376,378],{"class":377},"swJcz","template",[16,380,381],{"class":373},">\n",[16,383,384,387,390,394,397,400,404,406,409,411,413,416,418],{"class":369,"line":259},[16,385,386],{"class":373},"  \u003C",[16,388,389],{"class":377},"form",[16,391,393],{"class":392},"spNyl"," method",[16,395,396],{"class":373},"=",[16,398,399],{"class":373},"\"",[16,401,403],{"class":402},"sfazB","post",[16,405,399],{"class":373},[16,407,408],{"class":392}," action",[16,410,396],{"class":373},[16,412,399],{"class":373},[16,414,415],{"class":402},"\u002Ftag",[16,417,399],{"class":373},[16,419,381],{"class":373},[16,421,423,426,429,432,434,436,439,441,444,446,448,451,453,456],{"class":369,"line":422},3,[16,424,425],{"class":373},"    \u003C",[16,427,428],{"class":377},"input",[16,430,431],{"class":392}," type",[16,433,396],{"class":373},[16,435,399],{"class":373},[16,437,438],{"class":402},"text",[16,440,399],{"class":373},[16,442,443],{"class":392}," placeholder",[16,445,396],{"class":373},[16,447,399],{"class":373},[16,449,450],{"class":402},"Nom",[16,452,399],{"class":373},[16,454,455],{"class":392}," required",[16,457,381],{"class":373},[16,459,461,463,466,468,470,472,475,477],{"class":369,"line":460},4,[16,462,425],{"class":373},[16,464,465],{"class":377},"button",[16,467,431],{"class":392},[16,469,396],{"class":373},[16,471,399],{"class":373},[16,473,474],{"class":402},"submit",[16,476,399],{"class":373},[16,478,381],{"class":373},[16,480,482],{"class":369,"line":481},5,[16,483,485],{"class":484},"sTEyZ","      Créer Tag\n",[16,487,489,492,494],{"class":369,"line":488},6,[16,490,491],{"class":373},"    \u003C\u002F",[16,493,465],{"class":377},[16,495,381],{"class":373},[16,497,499,502,504],{"class":369,"line":498},7,[16,500,501],{"class":373},"  \u003C\u002F",[16,503,389],{"class":377},[16,505,381],{"class":373},[16,507,509,512,514],{"class":369,"line":508},8,[16,510,511],{"class":373},"\u003C\u002F",[16,513,378],{"class":377},[16,515,381],{"class":373},[13,517,518],{},"Ensuite, côté serveur, je peux créer une route pour gérer la soumission du formulaire :",[359,520,524],{"className":521,"code":522,"language":523,"meta":258,"style":258},"language-ts shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","export default defineEventHandler(async (event) => {\n  const body = await readBody(event)\n\n  \u002F\u002F Enregistrer dans la base de données...\n\n  return sendNoContent(event, 201)\n})\n","ts",[365,525,526,561,585,590,596,600,621],{"__ignoreMap":258},[16,527,528,532,535,539,542,545,548,552,555,558],{"class":369,"line":370},[16,529,531],{"class":530},"s7zQu","export",[16,533,534],{"class":530}," default",[16,536,538],{"class":537},"s2Zo4"," defineEventHandler",[16,540,541],{"class":484},"(",[16,543,544],{"class":392},"async",[16,546,547],{"class":373}," (",[16,549,551],{"class":550},"sHdIc","event",[16,553,554],{"class":373},")",[16,556,557],{"class":392}," =>",[16,559,560],{"class":373}," {\n",[16,562,563,566,569,572,575,578,580,582],{"class":369,"line":259},[16,564,565],{"class":392},"  const",[16,567,568],{"class":484}," body",[16,570,571],{"class":373}," =",[16,573,574],{"class":530}," await",[16,576,577],{"class":537}," readBody",[16,579,541],{"class":377},[16,581,551],{"class":484},[16,583,584],{"class":377},")\n",[16,586,587],{"class":369,"line":422},[16,588,589],{"emptyLinePlaceholder":268},"\n",[16,591,592],{"class":369,"line":460},[16,593,595],{"class":594},"sHwdD","  \u002F\u002F Enregistrer dans la base de données...\n",[16,597,598],{"class":369,"line":481},[16,599,589],{"emptyLinePlaceholder":268},[16,601,602,605,608,610,612,615,619],{"class":369,"line":488},[16,603,604],{"class":530},"  return",[16,606,607],{"class":537}," sendNoContent",[16,609,541],{"class":377},[16,611,551],{"class":484},[16,613,614],{"class":373},",",[16,616,618],{"class":617},"sbssI"," 201",[16,620,584],{"class":377},[16,622,623,626],{"class":369,"line":498},[16,624,625],{"class":373},"}",[16,627,584],{"class":484},[13,629,630,631],{},"Pour l'instant, je n'ai aucune validation. Je peux envoyer n'importe quoi au serveur, et il l'acceptera. ",[67,632,633],{},"C'est un énorme problème de sécurité.",[13,635,636],{},[67,637,638],{},"Rappelez-vous que la façon dont je gère la validation est spécifique à une SPA. Une MPA traditionnelle aurait ajouté des erreurs aux messages flash et redirigé la demande.",[640,641,643],"h3",{"id":642},"valider-le-corps","Valider le Corps",[13,645,646,647,150,652,655,656,25],{},"Pour valider le corps côté serveur, j'utilise ",[21,648,651],{"href":649,"rel":650},"https:\u002F\u002Fzod.dev",[33],"Zod",[67,653,654],{},"Zod est une validation de schéma en TypeScript avec inférence de type statique."," La bonne nouvelle est qu'h3, le routeur et le framework web qui alimentent Nitro, la couche serveur de Nuxt, fournit une utilitaire pour valider le corps : ",[365,657,658],{},"readValidatedBody",[359,660,662],{"className":521,"code":661,"language":523,"meta":258,"style":258},"import { z } from 'zod'\n\nexport default defineEventHandler(async (event) => {\n  const body = await readValidatedBody(event, z.object({\n    name: z.string(),\n  }).parse)\n\n  \u002F\u002F Enregistrer dans la base de données...\n\n  return sendNoContent(event, 201)\n})\n",[365,663,664,690,694,716,747,768,782,786,790,795,812],{"__ignoreMap":258},[16,665,666,669,672,675,678,681,684,687],{"class":369,"line":370},[16,667,668],{"class":530},"import",[16,670,671],{"class":373}," {",[16,673,674],{"class":484}," z",[16,676,677],{"class":373}," }",[16,679,680],{"class":530}," from",[16,682,683],{"class":373}," '",[16,685,686],{"class":402},"zod",[16,688,689],{"class":373},"'\n",[16,691,692],{"class":369,"line":259},[16,693,589],{"emptyLinePlaceholder":268},[16,695,696,698,700,702,704,706,708,710,712,714],{"class":369,"line":422},[16,697,531],{"class":530},[16,699,534],{"class":530},[16,701,538],{"class":537},[16,703,541],{"class":484},[16,705,544],{"class":392},[16,707,547],{"class":373},[16,709,551],{"class":550},[16,711,554],{"class":373},[16,713,557],{"class":392},[16,715,560],{"class":373},[16,717,718,720,722,724,726,729,731,733,735,737,739,742,744],{"class":369,"line":460},[16,719,565],{"class":392},[16,721,568],{"class":484},[16,723,571],{"class":373},[16,725,574],{"class":530},[16,727,728],{"class":537}," readValidatedBody",[16,730,541],{"class":377},[16,732,551],{"class":484},[16,734,614],{"class":373},[16,736,674],{"class":484},[16,738,25],{"class":373},[16,740,741],{"class":537},"object",[16,743,541],{"class":377},[16,745,746],{"class":373},"{\n",[16,748,749,752,755,757,759,762,765],{"class":369,"line":481},[16,750,751],{"class":377},"    name",[16,753,754],{"class":373},":",[16,756,674],{"class":484},[16,758,25],{"class":373},[16,760,761],{"class":537},"string",[16,763,764],{"class":377},"()",[16,766,767],{"class":373},",\n",[16,769,770,773,775,777,780],{"class":369,"line":488},[16,771,772],{"class":373},"  }",[16,774,554],{"class":377},[16,776,25],{"class":373},[16,778,779],{"class":484},"parse",[16,781,584],{"class":377},[16,783,784],{"class":369,"line":498},[16,785,589],{"emptyLinePlaceholder":268},[16,787,788],{"class":369,"line":508},[16,789,595],{"class":594},[16,791,793],{"class":369,"line":792},9,[16,794,589],{"emptyLinePlaceholder":268},[16,796,798,800,802,804,806,808,810],{"class":369,"line":797},10,[16,799,604],{"class":530},[16,801,607],{"class":537},[16,803,541],{"class":377},[16,805,551],{"class":484},[16,807,614],{"class":373},[16,809,618],{"class":617},[16,811,584],{"class":377},[16,813,815,817],{"class":369,"line":814},11,[16,816,625],{"class":373},[16,818,584],{"class":484},[13,820,821,822,824],{},"L'utilitaire ",[365,823,658],{}," lancera automatiquement une erreur si le corps ne correspond pas au schéma. Par exemple, si le nom est manquant, la requête échouera.",[826,827,831,834,841,1125,1132,1420,1429,1433,1436,1955,1979,1983,1986,1989,2000,2087,2095,2098,2101,2195,2198,2309,2312,2316,2323,2328,2332,2335,2468,2479,2496,3002,3005,3012,3287,3290,3725,3732,3735,3828,3832,3841,4023,4027,4030,4036,4039,4218,4221,4522,4525,4531,4882,4889,5151,5161,5506,5512,5684,5688,5704,5707],"callout",{"icon":828,"text":829,"to":830},"i-unjs-h3","Valider les Données avec H3","https:\u002F\u002Fh3.unjs.io\u002Fexamples\u002Fvalidate-data",[13,832,833],{},"Par défaut, l'erreur sera gérée par le serveur et lancera une erreur 500, mais je peux capturer l'erreur pour renvoyer un message d'erreur plus convivial.",[13,835,836,837,840],{},"La première solution consiste à utiliser la méthode ",[365,838,839],{},"safeParse"," qui ne lancera pas d'erreur mais renverra un résultat avec l'erreur le cas échéant.",[359,842,844],{"className":521,"code":843,"language":523,"meta":258,"style":258},"import { z } from 'zod'\n\nexport default defineEventHandler(async (event) => {\n  const result = await readValidatedBody(event, z.object({\n    name: z.string(),\n  }).safeParse)\n\n  if (!result.success) {\n    throw createError({\n      status: 400,\n      message: 'Données non valides',\n      data: result.error.errors[0].message,\n    })\n  }\n\n  const body = result.data\n\n  \u002F\u002F Enregistrer dans la base de données...\n\n  return sendNoContent(event, 201)\n})\n",[365,845,846,864,868,890,919,935,947,951,974,986,998,1015,1051,1059,1065,1070,1086,1091,1096,1101,1118],{"__ignoreMap":258},[16,847,848,850,852,854,856,858,860,862],{"class":369,"line":370},[16,849,668],{"class":530},[16,851,671],{"class":373},[16,853,674],{"class":484},[16,855,677],{"class":373},[16,857,680],{"class":530},[16,859,683],{"class":373},[16,861,686],{"class":402},[16,863,689],{"class":373},[16,865,866],{"class":369,"line":259},[16,867,589],{"emptyLinePlaceholder":268},[16,869,870,872,874,876,878,880,882,884,886,888],{"class":369,"line":422},[16,871,531],{"class":530},[16,873,534],{"class":530},[16,875,538],{"class":537},[16,877,541],{"class":484},[16,879,544],{"class":392},[16,881,547],{"class":373},[16,883,551],{"class":550},[16,885,554],{"class":373},[16,887,557],{"class":392},[16,889,560],{"class":373},[16,891,892,894,897,899,901,903,905,907,909,911,913,915,917],{"class":369,"line":460},[16,893,565],{"class":392},[16,895,896],{"class":484}," result",[16,898,571],{"class":373},[16,900,574],{"class":530},[16,902,728],{"class":537},[16,904,541],{"class":377},[16,906,551],{"class":484},[16,908,614],{"class":373},[16,910,674],{"class":484},[16,912,25],{"class":373},[16,914,741],{"class":537},[16,916,541],{"class":377},[16,918,746],{"class":373},[16,920,921,923,925,927,929,931,933],{"class":369,"line":481},[16,922,751],{"class":377},[16,924,754],{"class":373},[16,926,674],{"class":484},[16,928,25],{"class":373},[16,930,761],{"class":537},[16,932,764],{"class":377},[16,934,767],{"class":373},[16,936,937,939,941,943,945],{"class":369,"line":488},[16,938,772],{"class":373},[16,940,554],{"class":377},[16,942,25],{"class":373},[16,944,839],{"class":484},[16,946,584],{"class":377},[16,948,949],{"class":369,"line":498},[16,950,589],{"emptyLinePlaceholder":268},[16,952,953,956,958,961,964,966,969,972],{"class":369,"line":508},[16,954,955],{"class":530},"  if",[16,957,547],{"class":377},[16,959,960],{"class":373},"!",[16,962,963],{"class":484},"result",[16,965,25],{"class":373},[16,967,968],{"class":484},"success",[16,970,971],{"class":377},") ",[16,973,746],{"class":373},[16,975,976,979,982,984],{"class":369,"line":792},[16,977,978],{"class":530},"    throw",[16,980,981],{"class":537}," createError",[16,983,541],{"class":377},[16,985,746],{"class":373},[16,987,988,991,993,996],{"class":369,"line":797},[16,989,990],{"class":377},"      status",[16,992,754],{"class":373},[16,994,995],{"class":617}," 400",[16,997,767],{"class":373},[16,999,1000,1003,1005,1007,1010,1013],{"class":369,"line":814},[16,1001,1002],{"class":377},"      message",[16,1004,754],{"class":373},[16,1006,683],{"class":373},[16,1008,1009],{"class":402},"Données non valides",[16,1011,1012],{"class":373},"'",[16,1014,767],{"class":373},[16,1016,1018,1021,1023,1025,1027,1030,1032,1035,1038,1041,1044,1046,1049],{"class":369,"line":1017},12,[16,1019,1020],{"class":377},"      data",[16,1022,754],{"class":373},[16,1024,896],{"class":484},[16,1026,25],{"class":373},[16,1028,1029],{"class":484},"error",[16,1031,25],{"class":373},[16,1033,1034],{"class":484},"errors",[16,1036,1037],{"class":377},"[",[16,1039,1040],{"class":617},"0",[16,1042,1043],{"class":377},"]",[16,1045,25],{"class":373},[16,1047,1048],{"class":484},"message",[16,1050,767],{"class":373},[16,1052,1054,1057],{"class":369,"line":1053},13,[16,1055,1056],{"class":373},"    }",[16,1058,584],{"class":377},[16,1060,1062],{"class":369,"line":1061},14,[16,1063,1064],{"class":373},"  }\n",[16,1066,1068],{"class":369,"line":1067},15,[16,1069,589],{"emptyLinePlaceholder":268},[16,1071,1073,1075,1077,1079,1081,1083],{"class":369,"line":1072},16,[16,1074,565],{"class":392},[16,1076,568],{"class":484},[16,1078,571],{"class":373},[16,1080,896],{"class":484},[16,1082,25],{"class":373},[16,1084,1085],{"class":484},"data\n",[16,1087,1089],{"class":369,"line":1088},17,[16,1090,589],{"emptyLinePlaceholder":268},[16,1092,1094],{"class":369,"line":1093},18,[16,1095,595],{"class":594},[16,1097,1099],{"class":369,"line":1098},19,[16,1100,589],{"emptyLinePlaceholder":268},[16,1102,1104,1106,1108,1110,1112,1114,1116],{"class":369,"line":1103},20,[16,1105,604],{"class":530},[16,1107,607],{"class":537},[16,1109,541],{"class":377},[16,1111,551],{"class":484},[16,1113,614],{"class":373},[16,1115,618],{"class":617},[16,1117,584],{"class":377},[16,1119,1121,1123],{"class":369,"line":1120},21,[16,1122,625],{"class":373},[16,1124,584],{"class":484},[13,1126,1127,1128,1131],{},"La seconde solution consiste à utiliser la méthode ",[365,1129,1130],{},"parseAsync"," pour attacher une gestion d'erreur manuelle à la promesse.",[359,1133,1135],{"className":521,"code":1134,"language":523,"meta":258,"style":258},"import type { ZodError } from 'zod'\nimport { z } from 'zod'\n\nexport default defineEventHandler(async (event) => {\n  const body = await readValidatedBody(event, data => z.object({\n    name: z.string(),\n  }).parseAsync(data).catch((error) => {\n    if (error instanceof ZodError) {\n      throw createError({\n        status: 400,\n        message: 'Données non valides',\n        data: error.errors[0].message,\n      })\n    }\n  }))\n\n  \u002F\u002F Enregistrer dans la base de données...\n\n  return sendNoContent(event, 201)\n})\n",[365,1136,1137,1158,1176,1180,1202,1235,1251,1285,1304,1315,1326,1341,1367,1374,1379,1386,1390,1394,1398,1414],{"__ignoreMap":258},[16,1138,1139,1141,1143,1145,1148,1150,1152,1154,1156],{"class":369,"line":370},[16,1140,668],{"class":530},[16,1142,431],{"class":530},[16,1144,671],{"class":373},[16,1146,1147],{"class":484}," ZodError",[16,1149,677],{"class":373},[16,1151,680],{"class":530},[16,1153,683],{"class":373},[16,1155,686],{"class":402},[16,1157,689],{"class":373},[16,1159,1160,1162,1164,1166,1168,1170,1172,1174],{"class":369,"line":259},[16,1161,668],{"class":530},[16,1163,671],{"class":373},[16,1165,674],{"class":484},[16,1167,677],{"class":373},[16,1169,680],{"class":530},[16,1171,683],{"class":373},[16,1173,686],{"class":402},[16,1175,689],{"class":373},[16,1177,1178],{"class":369,"line":422},[16,1179,589],{"emptyLinePlaceholder":268},[16,1181,1182,1184,1186,1188,1190,1192,1194,1196,1198,1200],{"class":369,"line":460},[16,1183,531],{"class":530},[16,1185,534],{"class":530},[16,1187,538],{"class":537},[16,1189,541],{"class":484},[16,1191,544],{"class":392},[16,1193,547],{"class":373},[16,1195,551],{"class":550},[16,1197,554],{"class":373},[16,1199,557],{"class":392},[16,1201,560],{"class":373},[16,1203,1204,1206,1208,1210,1212,1214,1216,1218,1220,1223,1225,1227,1229,1231,1233],{"class":369,"line":481},[16,1205,565],{"class":392},[16,1207,568],{"class":484},[16,1209,571],{"class":373},[16,1211,574],{"class":530},[16,1213,728],{"class":537},[16,1215,541],{"class":377},[16,1217,551],{"class":484},[16,1219,614],{"class":373},[16,1221,1222],{"class":550}," data",[16,1224,557],{"class":392},[16,1226,674],{"class":484},[16,1228,25],{"class":373},[16,1230,741],{"class":537},[16,1232,541],{"class":377},[16,1234,746],{"class":373},[16,1236,1237,1239,1241,1243,1245,1247,1249],{"class":369,"line":488},[16,1238,751],{"class":377},[16,1240,754],{"class":373},[16,1242,674],{"class":484},[16,1244,25],{"class":373},[16,1246,761],{"class":537},[16,1248,764],{"class":377},[16,1250,767],{"class":373},[16,1252,1253,1255,1257,1259,1261,1263,1266,1268,1270,1273,1275,1277,1279,1281,1283],{"class":369,"line":498},[16,1254,772],{"class":373},[16,1256,554],{"class":377},[16,1258,25],{"class":373},[16,1260,1130],{"class":537},[16,1262,541],{"class":377},[16,1264,1265],{"class":484},"data",[16,1267,554],{"class":377},[16,1269,25],{"class":373},[16,1271,1272],{"class":537},"catch",[16,1274,541],{"class":377},[16,1276,541],{"class":373},[16,1278,1029],{"class":550},[16,1280,554],{"class":373},[16,1282,557],{"class":392},[16,1284,560],{"class":373},[16,1286,1287,1290,1292,1294,1297,1300,1302],{"class":369,"line":508},[16,1288,1289],{"class":530},"    if",[16,1291,547],{"class":377},[16,1293,1029],{"class":484},[16,1295,1296],{"class":373}," instanceof",[16,1298,1147],{"class":1299},"sBMFI",[16,1301,971],{"class":377},[16,1303,746],{"class":373},[16,1305,1306,1309,1311,1313],{"class":369,"line":792},[16,1307,1308],{"class":530},"      throw",[16,1310,981],{"class":537},[16,1312,541],{"class":377},[16,1314,746],{"class":373},[16,1316,1317,1320,1322,1324],{"class":369,"line":797},[16,1318,1319],{"class":377},"        status",[16,1321,754],{"class":373},[16,1323,995],{"class":617},[16,1325,767],{"class":373},[16,1327,1328,1331,1333,1335,1337,1339],{"class":369,"line":814},[16,1329,1330],{"class":377},"        message",[16,1332,754],{"class":373},[16,1334,683],{"class":373},[16,1336,1009],{"class":402},[16,1338,1012],{"class":373},[16,1340,767],{"class":373},[16,1342,1343,1346,1348,1351,1353,1355,1357,1359,1361,1363,1365],{"class":369,"line":1017},[16,1344,1345],{"class":377},"        data",[16,1347,754],{"class":373},[16,1349,1350],{"class":484}," error",[16,1352,25],{"class":373},[16,1354,1034],{"class":484},[16,1356,1037],{"class":377},[16,1358,1040],{"class":617},[16,1360,1043],{"class":377},[16,1362,25],{"class":373},[16,1364,1048],{"class":484},[16,1366,767],{"class":373},[16,1368,1369,1372],{"class":369,"line":1053},[16,1370,1371],{"class":373},"      }",[16,1373,584],{"class":377},[16,1375,1376],{"class":369,"line":1061},[16,1377,1378],{"class":373},"    }\n",[16,1380,1381,1383],{"class":369,"line":1067},[16,1382,772],{"class":373},[16,1384,1385],{"class":377},"))\n",[16,1387,1388],{"class":369,"line":1072},[16,1389,589],{"emptyLinePlaceholder":268},[16,1391,1392],{"class":369,"line":1088},[16,1393,595],{"class":594},[16,1395,1396],{"class":369,"line":1093},[16,1397,589],{"emptyLinePlaceholder":268},[16,1399,1400,1402,1404,1406,1408,1410,1412],{"class":369,"line":1098},[16,1401,604],{"class":530},[16,1403,607],{"class":537},[16,1405,541],{"class":377},[16,1407,551],{"class":484},[16,1409,614],{"class":373},[16,1411,618],{"class":617},[16,1413,584],{"class":377},[16,1415,1416,1418],{"class":369,"line":1103},[16,1417,625],{"class":373},[16,1419,584],{"class":484},[13,1421,1422],{},[67,1423,1424,1425,1428],{},"Vous pourriez envelopper le validateur avec un ",[365,1426,1427],{},"try\u002Fcatch",", mais je trouve cela moins élégant et lisible.",[640,1430,1432],{"id":1431},"valider-le-frontend","Valider le Frontend",[13,1434,1435],{},"Côté frontend, je peux également utiliser Zod pour valider le formulaire avant de l'envoyer au serveur.",[359,1437,1439],{"className":361,"code":1438,"language":363,"meta":258,"style":258},"\u003Cscript lang=\"ts\" setup>\nimport type { ZodError } from 'zod'\nimport { z } from 'zod'\n\nconst schema = z.object({\n  name: z.string({ message: 'Obligatoire' }), \u002F\u002F Je peux personnaliser le message d'erreur affiché à l'utilisateur\n})\n\nconst state = reactive({\n  name: '',\n})\n\nasync function onSubmit() {\n  try {\n    const data = schema.parse(state)\n    await $fetch('\u002Ftag', {\n      method: 'post',\n      body: data,\n    })\n  }\n  catch (error) {\n    if (error instanceof ZodError) {\n      throw createError({\n        status: 400,\n        message: 'Données non valides',\n        data: error.errors,\n      })\n    }\n  }\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cform @submit.prevent=\"onSubmit()\">\n    \u003Cinput v-model=\"state.name\" type=\"text\" placeholder=\"Nom\" required>\n    \u003Cbutton type=\"submit\">\n      Créer Tag\n    \u003C\u002Fbutton>\n  \u003C\u002Fform>\n\u003C\u002Ftemplate>\n",[365,1440,1441,1464,1484,1502,1506,1526,1565,1571,1575,1591,1602,1608,1612,1626,1633,1656,1676,1691,1702,1708,1712,1725,1742,1753,1764,1779,1794,1801,1806,1811,1817,1826,1831,1840,1861,1904,1923,1928,1937,1946],{"__ignoreMap":258},[16,1442,1443,1445,1448,1451,1453,1455,1457,1459,1462],{"class":369,"line":370},[16,1444,374],{"class":373},[16,1446,1447],{"class":377},"script",[16,1449,1450],{"class":392}," lang",[16,1452,396],{"class":373},[16,1454,399],{"class":373},[16,1456,523],{"class":402},[16,1458,399],{"class":373},[16,1460,1461],{"class":392}," setup",[16,1463,381],{"class":373},[16,1465,1466,1468,1470,1472,1474,1476,1478,1480,1482],{"class":369,"line":259},[16,1467,668],{"class":530},[16,1469,431],{"class":530},[16,1471,671],{"class":373},[16,1473,1147],{"class":484},[16,1475,677],{"class":373},[16,1477,680],{"class":530},[16,1479,683],{"class":373},[16,1481,686],{"class":402},[16,1483,689],{"class":373},[16,1485,1486,1488,1490,1492,1494,1496,1498,1500],{"class":369,"line":422},[16,1487,668],{"class":530},[16,1489,671],{"class":373},[16,1491,674],{"class":484},[16,1493,677],{"class":373},[16,1495,680],{"class":530},[16,1497,683],{"class":373},[16,1499,686],{"class":402},[16,1501,689],{"class":373},[16,1503,1504],{"class":369,"line":460},[16,1505,589],{"emptyLinePlaceholder":268},[16,1507,1508,1511,1514,1516,1518,1520,1522,1524],{"class":369,"line":481},[16,1509,1510],{"class":392},"const",[16,1512,1513],{"class":484}," schema ",[16,1515,396],{"class":373},[16,1517,674],{"class":484},[16,1519,25],{"class":373},[16,1521,741],{"class":537},[16,1523,541],{"class":484},[16,1525,746],{"class":373},[16,1527,1528,1531,1533,1535,1537,1539,1541,1544,1547,1549,1551,1554,1556,1558,1560,1562],{"class":369,"line":488},[16,1529,1530],{"class":377},"  name",[16,1532,754],{"class":373},[16,1534,674],{"class":484},[16,1536,25],{"class":373},[16,1538,761],{"class":537},[16,1540,541],{"class":484},[16,1542,1543],{"class":373},"{",[16,1545,1546],{"class":377}," message",[16,1548,754],{"class":373},[16,1550,683],{"class":373},[16,1552,1553],{"class":402},"Obligatoire",[16,1555,1012],{"class":373},[16,1557,677],{"class":373},[16,1559,554],{"class":484},[16,1561,614],{"class":373},[16,1563,1564],{"class":594}," \u002F\u002F Je peux personnaliser le message d'erreur affiché à l'utilisateur\n",[16,1566,1567,1569],{"class":369,"line":498},[16,1568,625],{"class":373},[16,1570,584],{"class":484},[16,1572,1573],{"class":369,"line":508},[16,1574,589],{"emptyLinePlaceholder":268},[16,1576,1577,1579,1582,1584,1587,1589],{"class":369,"line":792},[16,1578,1510],{"class":392},[16,1580,1581],{"class":484}," state ",[16,1583,396],{"class":373},[16,1585,1586],{"class":537}," reactive",[16,1588,541],{"class":484},[16,1590,746],{"class":373},[16,1592,1593,1595,1597,1600],{"class":369,"line":797},[16,1594,1530],{"class":377},[16,1596,754],{"class":373},[16,1598,1599],{"class":373}," ''",[16,1601,767],{"class":373},[16,1603,1604,1606],{"class":369,"line":814},[16,1605,625],{"class":373},[16,1607,584],{"class":484},[16,1609,1610],{"class":369,"line":1017},[16,1611,589],{"emptyLinePlaceholder":268},[16,1613,1614,1616,1619,1622,1624],{"class":369,"line":1053},[16,1615,544],{"class":392},[16,1617,1618],{"class":392}," function",[16,1620,1621],{"class":537}," onSubmit",[16,1623,764],{"class":373},[16,1625,560],{"class":373},[16,1627,1628,1631],{"class":369,"line":1061},[16,1629,1630],{"class":530},"  try",[16,1632,560],{"class":373},[16,1634,1635,1638,1640,1642,1645,1647,1649,1651,1654],{"class":369,"line":1067},[16,1636,1637],{"class":392},"    const",[16,1639,1222],{"class":484},[16,1641,571],{"class":373},[16,1643,1644],{"class":484}," schema",[16,1646,25],{"class":373},[16,1648,779],{"class":537},[16,1650,541],{"class":377},[16,1652,1653],{"class":484},"state",[16,1655,584],{"class":377},[16,1657,1658,1661,1664,1666,1668,1670,1672,1674],{"class":369,"line":1072},[16,1659,1660],{"class":530},"    await",[16,1662,1663],{"class":537}," $fetch",[16,1665,541],{"class":377},[16,1667,1012],{"class":373},[16,1669,415],{"class":402},[16,1671,1012],{"class":373},[16,1673,614],{"class":373},[16,1675,560],{"class":373},[16,1677,1678,1681,1683,1685,1687,1689],{"class":369,"line":1088},[16,1679,1680],{"class":377},"      method",[16,1682,754],{"class":373},[16,1684,683],{"class":373},[16,1686,403],{"class":402},[16,1688,1012],{"class":373},[16,1690,767],{"class":373},[16,1692,1693,1696,1698,1700],{"class":369,"line":1093},[16,1694,1695],{"class":377},"      body",[16,1697,754],{"class":373},[16,1699,1222],{"class":484},[16,1701,767],{"class":373},[16,1703,1704,1706],{"class":369,"line":1098},[16,1705,1056],{"class":373},[16,1707,584],{"class":377},[16,1709,1710],{"class":369,"line":1103},[16,1711,1064],{"class":373},[16,1713,1714,1717,1719,1721,1723],{"class":369,"line":1120},[16,1715,1716],{"class":530},"  catch",[16,1718,547],{"class":377},[16,1720,1029],{"class":484},[16,1722,971],{"class":377},[16,1724,746],{"class":373},[16,1726,1728,1730,1732,1734,1736,1738,1740],{"class":369,"line":1727},22,[16,1729,1289],{"class":530},[16,1731,547],{"class":377},[16,1733,1029],{"class":484},[16,1735,1296],{"class":373},[16,1737,1147],{"class":1299},[16,1739,971],{"class":377},[16,1741,746],{"class":373},[16,1743,1745,1747,1749,1751],{"class":369,"line":1744},23,[16,1746,1308],{"class":530},[16,1748,981],{"class":537},[16,1750,541],{"class":377},[16,1752,746],{"class":373},[16,1754,1756,1758,1760,1762],{"class":369,"line":1755},24,[16,1757,1319],{"class":377},[16,1759,754],{"class":373},[16,1761,995],{"class":617},[16,1763,767],{"class":373},[16,1765,1767,1769,1771,1773,1775,1777],{"class":369,"line":1766},25,[16,1768,1330],{"class":377},[16,1770,754],{"class":373},[16,1772,683],{"class":373},[16,1774,1009],{"class":402},[16,1776,1012],{"class":373},[16,1778,767],{"class":373},[16,1780,1782,1784,1786,1788,1790,1792],{"class":369,"line":1781},26,[16,1783,1345],{"class":377},[16,1785,754],{"class":373},[16,1787,1350],{"class":484},[16,1789,25],{"class":373},[16,1791,1034],{"class":484},[16,1793,767],{"class":373},[16,1795,1797,1799],{"class":369,"line":1796},27,[16,1798,1371],{"class":373},[16,1800,584],{"class":377},[16,1802,1804],{"class":369,"line":1803},28,[16,1805,1378],{"class":373},[16,1807,1809],{"class":369,"line":1808},29,[16,1810,1064],{"class":373},[16,1812,1814],{"class":369,"line":1813},30,[16,1815,1816],{"class":373},"}\n",[16,1818,1820,1822,1824],{"class":369,"line":1819},31,[16,1821,511],{"class":373},[16,1823,1447],{"class":377},[16,1825,381],{"class":373},[16,1827,1829],{"class":369,"line":1828},32,[16,1830,589],{"emptyLinePlaceholder":268},[16,1832,1834,1836,1838],{"class":369,"line":1833},33,[16,1835,374],{"class":373},[16,1837,378],{"class":377},[16,1839,381],{"class":373},[16,1841,1843,1845,1847,1850,1852,1854,1857,1859],{"class":369,"line":1842},34,[16,1844,386],{"class":373},[16,1846,389],{"class":377},[16,1848,1849],{"class":392}," @submit.prevent",[16,1851,396],{"class":373},[16,1853,399],{"class":373},[16,1855,1856],{"class":402},"onSubmit()",[16,1858,399],{"class":373},[16,1860,381],{"class":373},[16,1862,1864,1866,1868,1871,1873,1875,1878,1880,1882,1884,1886,1888,1890,1892,1894,1896,1898,1900,1902],{"class":369,"line":1863},35,[16,1865,425],{"class":373},[16,1867,428],{"class":377},[16,1869,1870],{"class":392}," v-model",[16,1872,396],{"class":373},[16,1874,399],{"class":373},[16,1876,1877],{"class":402},"state.name",[16,1879,399],{"class":373},[16,1881,431],{"class":392},[16,1883,396],{"class":373},[16,1885,399],{"class":373},[16,1887,438],{"class":402},[16,1889,399],{"class":373},[16,1891,443],{"class":392},[16,1893,396],{"class":373},[16,1895,399],{"class":373},[16,1897,450],{"class":402},[16,1899,399],{"class":373},[16,1901,455],{"class":392},[16,1903,381],{"class":373},[16,1905,1907,1909,1911,1913,1915,1917,1919,1921],{"class":369,"line":1906},36,[16,1908,425],{"class":373},[16,1910,465],{"class":377},[16,1912,431],{"class":392},[16,1914,396],{"class":373},[16,1916,399],{"class":373},[16,1918,474],{"class":402},[16,1920,399],{"class":373},[16,1922,381],{"class":373},[16,1924,1926],{"class":369,"line":1925},37,[16,1927,485],{"class":484},[16,1929,1931,1933,1935],{"class":369,"line":1930},38,[16,1932,491],{"class":373},[16,1934,465],{"class":377},[16,1936,381],{"class":373},[16,1938,1940,1942,1944],{"class":369,"line":1939},39,[16,1941,501],{"class":373},[16,1943,389],{"class":377},[16,1945,381],{"class":373},[16,1947,1949,1951,1953],{"class":369,"line":1948},40,[16,1950,511],{"class":373},[16,1952,378],{"class":377},[16,1954,381],{"class":373},[13,1956,1957,1958,1961,1962,1967,1968,1973,1974,25],{},"L'implémentation est très simple et directe. Je pourrais enregistrer les erreurs dans une variable pour les afficher à l'utilisateur. Si vous souhaitez aller plus loin, je vous recommande d'essayer le composant ",[365,1959,1960],{},"Form"," de ",[21,1963,1966],{"href":1964,"rel":1965},"https:\u002F\u002Fui.nuxt.com\u002F",[33],"Nuxt UI"," ou une bibliothèque de validation comme ",[21,1969,1972],{"href":1970,"rel":1971},"https:\u002F\u002Fvee-validate.logaretm.com\u002Fv4\u002F",[33],"VeeValidate"," ou ",[21,1975,1978],{"href":1976,"rel":1977},"https:\u002F\u002Fformkit.com\u002F",[33],"FormKit",[103,1980,1982],{"id":1981},"refactorisons","Refactorisons",[13,1984,1985],{},"Maintenant que j'ai la logique de validation à la fois côté client et côté serveur, voyons s'il y a un code intéressant à refactoriser.",[13,1987,1988],{},"La première chose que je remarque est que la logique de validation, côté client et serveur, est la même, et c'est totalement normal. Mais que se passe-t-il si je décide de changer la logique de validation côté serveur parce que mon API est utilisée par une autre application ? Mon frontend pourrait être cassé car la logique de validation sera différente. Pour éviter cela, il serait peut-être mieux d'écrire le validateur une seule fois et de l'utiliser des deux côtés. Qu'en pensez-vous ?",[13,1990,1991,1992,1995,1996,1999],{},"Je peux créer un fichier ",[365,1993,1994],{},"validators.ts"," dans le dossier ",[365,1997,1998],{},"utils"," de la partie client (à la racine) d'une application Nuxt. Ce fichier contiendra tous les validateurs pour mon application et sera partagé entre le client et le serveur.",[359,2001,2003],{"className":521,"code":2002,"language":523,"meta":258,"style":258},"import { z } from 'zod'\n\nexport const createTagValidator = z.object({\n  name: z.string({ message: 'Obligatoire' }),\n})\n",[365,2004,2005,2023,2027,2049,2081],{"__ignoreMap":258},[16,2006,2007,2009,2011,2013,2015,2017,2019,2021],{"class":369,"line":370},[16,2008,668],{"class":530},[16,2010,671],{"class":373},[16,2012,674],{"class":484},[16,2014,677],{"class":373},[16,2016,680],{"class":530},[16,2018,683],{"class":373},[16,2020,686],{"class":402},[16,2022,689],{"class":373},[16,2024,2025],{"class":369,"line":259},[16,2026,589],{"emptyLinePlaceholder":268},[16,2028,2029,2031,2034,2037,2039,2041,2043,2045,2047],{"class":369,"line":422},[16,2030,531],{"class":530},[16,2032,2033],{"class":392}," const",[16,2035,2036],{"class":484}," createTagValidator ",[16,2038,396],{"class":373},[16,2040,674],{"class":484},[16,2042,25],{"class":373},[16,2044,741],{"class":537},[16,2046,541],{"class":484},[16,2048,746],{"class":373},[16,2050,2051,2053,2055,2057,2059,2061,2063,2065,2067,2069,2071,2073,2075,2077,2079],{"class":369,"line":460},[16,2052,1530],{"class":377},[16,2054,754],{"class":373},[16,2056,674],{"class":484},[16,2058,25],{"class":373},[16,2060,761],{"class":537},[16,2062,541],{"class":484},[16,2064,1543],{"class":373},[16,2066,1546],{"class":377},[16,2068,754],{"class":373},[16,2070,683],{"class":373},[16,2072,1553],{"class":402},[16,2074,1012],{"class":373},[16,2076,677],{"class":373},[16,2078,554],{"class":484},[16,2080,767],{"class":373},[16,2082,2083,2085],{"class":369,"line":481},[16,2084,625],{"class":373},[16,2086,584],{"class":484},[10,2088,2089],{},[13,2090,2091,2094],{},[16,2092,2093],{},"!TIP","\nCe fichier peut être utilisé côté serveur mais a été créé côté client, donc vous devez explicitement importer tout ce dont vous avez besoin pour éviter des problèmes de rendu.",[13,2096,2097],{},"Ce validateur contient le message pour l'erreur pour aider l'utilisateur à comprendre ce qui ne va pas.",[13,2099,2100],{},"Grâce à la fonction d'auto-import de Nuxt, je peux facilement l'utiliser côté client :",[359,2102,2104],{"className":361,"code":2103,"language":363,"meta":258,"style":258},"\u003Cscript lang=\"ts\" setup>\nconst schema = createTagValidator\n\n\u002F\u002F ...\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cform>\n  \u003C!-- ... -->\n  \u003C\u002Fform>\n\u003C\u002Ftemplate>\n",[365,2105,2106,2126,2137,2141,2146,2154,2158,2166,2174,2179,2187],{"__ignoreMap":258},[16,2107,2108,2110,2112,2114,2116,2118,2120,2122,2124],{"class":369,"line":370},[16,2109,374],{"class":373},[16,2111,1447],{"class":377},[16,2113,1450],{"class":392},[16,2115,396],{"class":373},[16,2117,399],{"class":373},[16,2119,523],{"class":402},[16,2121,399],{"class":373},[16,2123,1461],{"class":392},[16,2125,381],{"class":373},[16,2127,2128,2130,2132,2134],{"class":369,"line":259},[16,2129,1510],{"class":392},[16,2131,1513],{"class":484},[16,2133,396],{"class":373},[16,2135,2136],{"class":484}," createTagValidator\n",[16,2138,2139],{"class":369,"line":422},[16,2140,589],{"emptyLinePlaceholder":268},[16,2142,2143],{"class":369,"line":460},[16,2144,2145],{"class":594},"\u002F\u002F ...\n",[16,2147,2148,2150,2152],{"class":369,"line":481},[16,2149,511],{"class":373},[16,2151,1447],{"class":377},[16,2153,381],{"class":373},[16,2155,2156],{"class":369,"line":488},[16,2157,589],{"emptyLinePlaceholder":268},[16,2159,2160,2162,2164],{"class":369,"line":498},[16,2161,374],{"class":373},[16,2163,378],{"class":377},[16,2165,381],{"class":373},[16,2167,2168,2170,2172],{"class":369,"line":508},[16,2169,386],{"class":373},[16,2171,389],{"class":377},[16,2173,381],{"class":373},[16,2175,2176],{"class":369,"line":792},[16,2177,2178],{"class":594},"  \u003C!-- ... -->\n",[16,2180,2181,2183,2185],{"class":369,"line":797},[16,2182,501],{"class":373},[16,2184,389],{"class":377},[16,2186,381],{"class":373},[16,2188,2189,2191,2193],{"class":369,"line":814},[16,2190,511],{"class":373},[16,2192,378],{"class":377},[16,2194,381],{"class":373},[13,2196,2197],{},"Ensuite, vous pouvez également l'utiliser côté serveur en l'importation :",[359,2199,2201],{"className":521,"code":2200,"language":523,"meta":258,"style":258},"import { createTagValidator } from '~\u002Futils\u002Fvalidators'\n\nexport default defineEventHandler(async (event) => {\n  const body = await readValidatedBody(event, createTagValidator.parse)\n\n  \u002F\u002F Enregistrer dans la base de données...\n\n  return sendNoContent(event, 201)\n})\n",[365,2202,2203,2223,2227,2249,2275,2279,2283,2287,2303],{"__ignoreMap":258},[16,2204,2205,2207,2209,2212,2214,2216,2218,2221],{"class":369,"line":370},[16,2206,668],{"class":530},[16,2208,671],{"class":373},[16,2210,2211],{"class":484}," createTagValidator",[16,2213,677],{"class":373},[16,2215,680],{"class":530},[16,2217,683],{"class":373},[16,2219,2220],{"class":402},"~\u002Futils\u002Fvalidators",[16,2222,689],{"class":373},[16,2224,2225],{"class":369,"line":259},[16,2226,589],{"emptyLinePlaceholder":268},[16,2228,2229,2231,2233,2235,2237,2239,2241,2243,2245,2247],{"class":369,"line":422},[16,2230,531],{"class":530},[16,2232,534],{"class":530},[16,2234,538],{"class":537},[16,2236,541],{"class":484},[16,2238,544],{"class":392},[16,2240,547],{"class":373},[16,2242,551],{"class":550},[16,2244,554],{"class":373},[16,2246,557],{"class":392},[16,2248,560],{"class":373},[16,2250,2251,2253,2255,2257,2259,2261,2263,2265,2267,2269,2271,2273],{"class":369,"line":460},[16,2252,565],{"class":392},[16,2254,568],{"class":484},[16,2256,571],{"class":373},[16,2258,574],{"class":530},[16,2260,728],{"class":537},[16,2262,541],{"class":377},[16,2264,551],{"class":484},[16,2266,614],{"class":373},[16,2268,2211],{"class":484},[16,2270,25],{"class":373},[16,2272,779],{"class":484},[16,2274,584],{"class":377},[16,2276,2277],{"class":369,"line":481},[16,2278,589],{"emptyLinePlaceholder":268},[16,2280,2281],{"class":369,"line":488},[16,2282,595],{"class":594},[16,2284,2285],{"class":369,"line":498},[16,2286,589],{"emptyLinePlaceholder":268},[16,2288,2289,2291,2293,2295,2297,2299,2301],{"class":369,"line":508},[16,2290,604],{"class":530},[16,2292,607],{"class":537},[16,2294,541],{"class":377},[16,2296,551],{"class":484},[16,2298,614],{"class":373},[16,2300,618],{"class":617},[16,2302,584],{"class":377},[16,2304,2305,2307],{"class":369,"line":792},[16,2306,625],{"class":373},[16,2308,584],{"class":484},[13,2310,2311],{},"Maintenant, je peux changer en toute sécurité la logique de validation sans casser le frontend, car ils suivent les mêmes règles, et je n'ai pas à dupliquer le code.",[103,2313,2315],{"id":2314},"gestion-des-fichiers","Gestion des Fichiers",[13,2317,2318,2319,2322],{},"Pour cette partie, je vais vous montrer deux façons différentes de valider les fichiers côté serveur. La première utilisera Zod et quelques raffinements, et la seconde utilisera l'utilitaire ",[365,2320,2321],{},"ensureBlob"," de NuxtHub.",[13,2324,2325],{},[67,2326,2327],{},"Cette validation n'est pas complète et devrait être améliorée pour gérer plus de cas.",[640,2329,2331],{"id":2330},"avec-zod","Avec Zod",[13,2333,2334],{},"Pour cette seconde partie, je vais vous montrer comment télécharger un fichier sur le serveur. D'abord, ajoutons notre entrée dans le formulaire :",[359,2336,2338],{"className":361,"code":2337,"language":363,"meta":258,"style":258},"\u003Ctemplate>\n  \u003Cform action=\"\u002Fapi\u002Fimage\" enctype=\"multipart\u002Fform-data\" method=\"post\">\n    \u003Cinput type=\"file\" accept=\"image\u002Fpng, image\u002Fjpeg, image\u002Fjpg\" required>\n    \u003Cbutton type=\"submit\">\n      Créer Tag\n    \u003C\u002Fbutton>\n  \u003C\u002Fform>\n\u003C\u002Ftemplate>\n",[365,2339,2340,2348,2389,2422,2440,2444,2452,2460],{"__ignoreMap":258},[16,2341,2342,2344,2346],{"class":369,"line":370},[16,2343,374],{"class":373},[16,2345,378],{"class":377},[16,2347,381],{"class":373},[16,2349,2350,2352,2354,2356,2358,2360,2363,2365,2368,2370,2372,2375,2377,2379,2381,2383,2385,2387],{"class":369,"line":259},[16,2351,386],{"class":373},[16,2353,389],{"class":377},[16,2355,408],{"class":392},[16,2357,396],{"class":373},[16,2359,399],{"class":373},[16,2361,2362],{"class":402},"\u002Fapi\u002Fimage",[16,2364,399],{"class":373},[16,2366,2367],{"class":392}," enctype",[16,2369,396],{"class":373},[16,2371,399],{"class":373},[16,2373,2374],{"class":402},"multipart\u002Fform-data",[16,2376,399],{"class":373},[16,2378,393],{"class":392},[16,2380,396],{"class":373},[16,2382,399],{"class":373},[16,2384,403],{"class":402},[16,2386,399],{"class":373},[16,2388,381],{"class":373},[16,2390,2391,2393,2395,2397,2399,2401,2404,2406,2409,2411,2413,2416,2418,2420],{"class":369,"line":422},[16,2392,425],{"class":373},[16,2394,428],{"class":377},[16,2396,431],{"class":392},[16,2398,396],{"class":373},[16,2400,399],{"class":373},[16,2402,2403],{"class":402},"file",[16,2405,399],{"class":373},[16,2407,2408],{"class":392}," accept",[16,2410,396],{"class":373},[16,2412,399],{"class":373},[16,2414,2415],{"class":402},"image\u002Fpng, image\u002Fjpeg, image\u002Fjpg",[16,2417,399],{"class":373},[16,2419,455],{"class":392},[16,2421,381],{"class":373},[16,2423,2424,2426,2428,2430,2432,2434,2436,2438],{"class":369,"line":460},[16,2425,425],{"class":373},[16,2427,465],{"class":377},[16,2429,431],{"class":392},[16,2431,396],{"class":373},[16,2433,399],{"class":373},[16,2435,474],{"class":402},[16,2437,399],{"class":373},[16,2439,381],{"class":373},[16,2441,2442],{"class":369,"line":481},[16,2443,485],{"class":484},[16,2445,2446,2448,2450],{"class":369,"line":488},[16,2447,491],{"class":373},[16,2449,465],{"class":377},[16,2451,381],{"class":373},[16,2453,2454,2456,2458],{"class":369,"line":498},[16,2455,501],{"class":373},[16,2457,389],{"class":377},[16,2459,381],{"class":373},[16,2461,2462,2464,2466],{"class":369,"line":508},[16,2463,511],{"class":373},[16,2465,378],{"class":377},[16,2467,381],{"class":373},[13,2469,2470,2471,2474,2475,2478],{},"Ensuite, je peux gérer le fichier côté serveur, mais c'est un peu plus complexe car j'envoie un ",[365,2472,2473],{},"formData"," et h3 ne fournit pas d'utilitaire comme ",[365,2476,2477],{},"readValidatedFormData",". Donc, je dois le gérer manuellement.",[13,2480,2481,2482,2484,2485,2488,2489,2492,2493,2495],{},"D'abord, je vais lire le ",[365,2483,2473],{}," en utilisant l'utilitaire ",[365,2486,2487],{},"readFormData",", puis j'extraire l'",[365,2490,2491],{},"image"," du ",[365,2494,2473],{}," et la valider en utilisant Zod.",[359,2497,2499],{"className":521,"code":2498,"language":523,"meta":258,"style":258},"import { any, object } from 'zod'\n\nexport default defineEventHandler(async (event) => {\n  const formData = await readFormData(event)\n\n  const image = formData.get('image')\n  await object({\n    image: any()\n      .refine(image => image instanceof File, { message: 'L\\'image doit être un fichier' })\n      .refine(image => image.size \u003C 1024 * 1024, { message: 'L\\'image doit faire moins de 1 Mo' })\n      .refine(image => image.type.startsWith('image\u002F'), { message: 'L\\'image doit être une image' })\n      .refine(image => ['image\u002Fpng', 'image\u002Fjpeg', 'image\u002Fjpg'].includes(image.type), { message: 'L\\'image doit être un JPEG ou un PNG' })\n  }).parseAsync({ image }).catch((error) => {\n    throw createError({\n      status: 400,\n      message: 'Image non valide',\n      data: error.errors[0].message\n    })\n  })\n\n  \u002F\u002F Enregistrer l'image dans une base de données ou un système de fichiers\n\n  return sendNoContent(event, 201)\n})\n",[365,2500,2501,2525,2529,2551,2571,2575,2601,2612,2624,2670,2723,2781,2861,2897,2907,2917,2932,2955,2961,2967,2971,2976,2980,2996],{"__ignoreMap":258},[16,2502,2503,2505,2507,2510,2512,2515,2517,2519,2521,2523],{"class":369,"line":370},[16,2504,668],{"class":530},[16,2506,671],{"class":373},[16,2508,2509],{"class":484}," any",[16,2511,614],{"class":373},[16,2513,2514],{"class":484}," object",[16,2516,677],{"class":373},[16,2518,680],{"class":530},[16,2520,683],{"class":373},[16,2522,686],{"class":402},[16,2524,689],{"class":373},[16,2526,2527],{"class":369,"line":259},[16,2528,589],{"emptyLinePlaceholder":268},[16,2530,2531,2533,2535,2537,2539,2541,2543,2545,2547,2549],{"class":369,"line":422},[16,2532,531],{"class":530},[16,2534,534],{"class":530},[16,2536,538],{"class":537},[16,2538,541],{"class":484},[16,2540,544],{"class":392},[16,2542,547],{"class":373},[16,2544,551],{"class":550},[16,2546,554],{"class":373},[16,2548,557],{"class":392},[16,2550,560],{"class":373},[16,2552,2553,2555,2558,2560,2562,2565,2567,2569],{"class":369,"line":460},[16,2554,565],{"class":392},[16,2556,2557],{"class":484}," formData",[16,2559,571],{"class":373},[16,2561,574],{"class":530},[16,2563,2564],{"class":537}," readFormData",[16,2566,541],{"class":377},[16,2568,551],{"class":484},[16,2570,584],{"class":377},[16,2572,2573],{"class":369,"line":481},[16,2574,589],{"emptyLinePlaceholder":268},[16,2576,2577,2579,2582,2584,2586,2588,2591,2593,2595,2597,2599],{"class":369,"line":488},[16,2578,565],{"class":392},[16,2580,2581],{"class":484}," image",[16,2583,571],{"class":373},[16,2585,2557],{"class":484},[16,2587,25],{"class":373},[16,2589,2590],{"class":537},"get",[16,2592,541],{"class":377},[16,2594,1012],{"class":373},[16,2596,2491],{"class":402},[16,2598,1012],{"class":373},[16,2600,584],{"class":377},[16,2602,2603,2606,2608,2610],{"class":369,"line":498},[16,2604,2605],{"class":530},"  await",[16,2607,2514],{"class":537},[16,2609,541],{"class":377},[16,2611,746],{"class":373},[16,2613,2614,2617,2619,2621],{"class":369,"line":508},[16,2615,2616],{"class":377},"    image",[16,2618,754],{"class":373},[16,2620,2509],{"class":537},[16,2622,2623],{"class":377},"()\n",[16,2625,2626,2629,2632,2634,2636,2638,2640,2642,2645,2647,2649,2651,2653,2655,2658,2661,2664,2666,2668],{"class":369,"line":792},[16,2627,2628],{"class":373},"      .",[16,2630,2631],{"class":537},"refine",[16,2633,541],{"class":377},[16,2635,2491],{"class":550},[16,2637,557],{"class":392},[16,2639,2581],{"class":484},[16,2641,1296],{"class":373},[16,2643,2644],{"class":1299}," File",[16,2646,614],{"class":373},[16,2648,671],{"class":373},[16,2650,1546],{"class":377},[16,2652,754],{"class":373},[16,2654,683],{"class":373},[16,2656,2657],{"class":402},"L",[16,2659,2660],{"class":484},"\\'",[16,2662,2663],{"class":402},"image doit être un fichier",[16,2665,1012],{"class":373},[16,2667,677],{"class":373},[16,2669,584],{"class":377},[16,2671,2672,2674,2676,2678,2680,2682,2684,2686,2689,2692,2695,2698,2700,2702,2704,2706,2708,2710,2712,2714,2717,2719,2721],{"class":369,"line":797},[16,2673,2628],{"class":373},[16,2675,2631],{"class":537},[16,2677,541],{"class":377},[16,2679,2491],{"class":550},[16,2681,557],{"class":392},[16,2683,2581],{"class":484},[16,2685,25],{"class":373},[16,2687,2688],{"class":484},"size",[16,2690,2691],{"class":373}," \u003C",[16,2693,2694],{"class":617}," 1024",[16,2696,2697],{"class":373}," *",[16,2699,2694],{"class":617},[16,2701,614],{"class":373},[16,2703,671],{"class":373},[16,2705,1546],{"class":377},[16,2707,754],{"class":373},[16,2709,683],{"class":373},[16,2711,2657],{"class":402},[16,2713,2660],{"class":484},[16,2715,2716],{"class":402},"image doit faire moins de 1 Mo",[16,2718,1012],{"class":373},[16,2720,677],{"class":373},[16,2722,584],{"class":377},[16,2724,2725,2727,2729,2731,2733,2735,2737,2739,2742,2744,2747,2749,2751,2754,2756,2758,2760,2762,2764,2766,2768,2770,2772,2775,2777,2779],{"class":369,"line":814},[16,2726,2628],{"class":373},[16,2728,2631],{"class":537},[16,2730,541],{"class":377},[16,2732,2491],{"class":550},[16,2734,557],{"class":392},[16,2736,2581],{"class":484},[16,2738,25],{"class":373},[16,2740,2741],{"class":484},"type",[16,2743,25],{"class":373},[16,2745,2746],{"class":537},"startsWith",[16,2748,541],{"class":377},[16,2750,1012],{"class":373},[16,2752,2753],{"class":402},"image\u002F",[16,2755,1012],{"class":373},[16,2757,554],{"class":377},[16,2759,614],{"class":373},[16,2761,671],{"class":373},[16,2763,1546],{"class":377},[16,2765,754],{"class":373},[16,2767,683],{"class":373},[16,2769,2657],{"class":402},[16,2771,2660],{"class":484},[16,2773,2774],{"class":402},"image doit être une image",[16,2776,1012],{"class":373},[16,2778,677],{"class":373},[16,2780,584],{"class":377},[16,2782,2783,2785,2787,2789,2791,2793,2796,2798,2801,2803,2805,2807,2810,2812,2814,2816,2819,2821,2823,2825,2828,2830,2832,2834,2836,2838,2840,2842,2844,2846,2848,2850,2852,2855,2857,2859],{"class":369,"line":1017},[16,2784,2628],{"class":373},[16,2786,2631],{"class":537},[16,2788,541],{"class":377},[16,2790,2491],{"class":550},[16,2792,557],{"class":392},[16,2794,2795],{"class":377}," [",[16,2797,1012],{"class":373},[16,2799,2800],{"class":402},"image\u002Fpng",[16,2802,1012],{"class":373},[16,2804,614],{"class":373},[16,2806,683],{"class":373},[16,2808,2809],{"class":402},"image\u002Fjpeg",[16,2811,1012],{"class":373},[16,2813,614],{"class":373},[16,2815,683],{"class":373},[16,2817,2818],{"class":402},"image\u002Fjpg",[16,2820,1012],{"class":373},[16,2822,1043],{"class":377},[16,2824,25],{"class":373},[16,2826,2827],{"class":537},"includes",[16,2829,541],{"class":377},[16,2831,2491],{"class":484},[16,2833,25],{"class":373},[16,2835,2741],{"class":484},[16,2837,554],{"class":377},[16,2839,614],{"class":373},[16,2841,671],{"class":373},[16,2843,1546],{"class":377},[16,2845,754],{"class":373},[16,2847,683],{"class":373},[16,2849,2657],{"class":402},[16,2851,2660],{"class":484},[16,2853,2854],{"class":402},"image doit être un JPEG ou un PNG",[16,2856,1012],{"class":373},[16,2858,677],{"class":373},[16,2860,584],{"class":377},[16,2862,2863,2865,2867,2869,2871,2873,2875,2877,2879,2881,2883,2885,2887,2889,2891,2893,2895],{"class":369,"line":1053},[16,2864,772],{"class":373},[16,2866,554],{"class":377},[16,2868,25],{"class":373},[16,2870,1130],{"class":537},[16,2872,541],{"class":377},[16,2874,1543],{"class":373},[16,2876,2581],{"class":484},[16,2878,677],{"class":373},[16,2880,554],{"class":377},[16,2882,25],{"class":373},[16,2884,1272],{"class":537},[16,2886,541],{"class":377},[16,2888,541],{"class":373},[16,2890,1029],{"class":550},[16,2892,554],{"class":373},[16,2894,557],{"class":392},[16,2896,560],{"class":373},[16,2898,2899,2901,2903,2905],{"class":369,"line":1061},[16,2900,978],{"class":530},[16,2902,981],{"class":537},[16,2904,541],{"class":377},[16,2906,746],{"class":373},[16,2908,2909,2911,2913,2915],{"class":369,"line":1067},[16,2910,990],{"class":377},[16,2912,754],{"class":373},[16,2914,995],{"class":617},[16,2916,767],{"class":373},[16,2918,2919,2921,2923,2925,2928,2930],{"class":369,"line":1072},[16,2920,1002],{"class":377},[16,2922,754],{"class":373},[16,2924,683],{"class":373},[16,2926,2927],{"class":402},"Image non valide",[16,2929,1012],{"class":373},[16,2931,767],{"class":373},[16,2933,2934,2936,2938,2940,2942,2944,2946,2948,2950,2952],{"class":369,"line":1088},[16,2935,1020],{"class":377},[16,2937,754],{"class":373},[16,2939,1350],{"class":484},[16,2941,25],{"class":373},[16,2943,1034],{"class":484},[16,2945,1037],{"class":377},[16,2947,1040],{"class":617},[16,2949,1043],{"class":377},[16,2951,25],{"class":373},[16,2953,2954],{"class":484},"message\n",[16,2956,2957,2959],{"class":369,"line":1093},[16,2958,1056],{"class":373},[16,2960,584],{"class":377},[16,2962,2963,2965],{"class":369,"line":1098},[16,2964,772],{"class":373},[16,2966,584],{"class":377},[16,2968,2969],{"class":369,"line":1103},[16,2970,589],{"emptyLinePlaceholder":268},[16,2972,2973],{"class":369,"line":1120},[16,2974,2975],{"class":594},"  \u002F\u002F Enregistrer l'image dans une base de données ou un système de fichiers\n",[16,2977,2978],{"class":369,"line":1727},[16,2979,589],{"emptyLinePlaceholder":268},[16,2981,2982,2984,2986,2988,2990,2992,2994],{"class":369,"line":1744},[16,2983,604],{"class":530},[16,2985,607],{"class":537},[16,2987,541],{"class":377},[16,2989,551],{"class":484},[16,2991,614],{"class":373},[16,2993,618],{"class":617},[16,2995,584],{"class":377},[16,2997,2998,3000],{"class":369,"line":1755},[16,2999,625],{"class":373},[16,3001,584],{"class":484},[13,3003,3004],{},"Côté client, je peux également valider le fichier en utilisant la même technique qu'auparavant et en partageant le validateur entre le client et le serveur.",[13,3006,3007,3008,3011],{},"D'abord, je dois déplacer le validateur dans le fichier ",[365,3009,3010],{},"utils\u002Fvalidators.ts"," :",[359,3013,3015],{"className":521,"code":3014,"language":523,"meta":258,"style":258},"import { any, object } from 'zod'\n\nexport const createImageValidator = object({\n  image: any()\n    .refine(image => image instanceof File, { message: 'L\\'image doit être un fichier' })\n    .refine(image => image.size \u003C 1024 * 1024, { message: 'L\\'image doit faire moins de 1 Mo' })\n    .refine(image => image.type.startsWith('image\u002F'), { message: 'L\\'image doit être une image' })\n    .refine(image => ['image\u002Fjpeg', 'image\u002Fpng'].includes(image.type), { message: 'L\\'image doit être un JPEG ou un PNG' })\n})\n",[365,3016,3017,3039,3043,3060,3071,3114,3163,3217,3281],{"__ignoreMap":258},[16,3018,3019,3021,3023,3025,3027,3029,3031,3033,3035,3037],{"class":369,"line":370},[16,3020,668],{"class":530},[16,3022,671],{"class":373},[16,3024,2509],{"class":484},[16,3026,614],{"class":373},[16,3028,2514],{"class":484},[16,3030,677],{"class":373},[16,3032,680],{"class":530},[16,3034,683],{"class":373},[16,3036,686],{"class":402},[16,3038,689],{"class":373},[16,3040,3041],{"class":369,"line":259},[16,3042,589],{"emptyLinePlaceholder":268},[16,3044,3045,3047,3049,3052,3054,3056,3058],{"class":369,"line":422},[16,3046,531],{"class":530},[16,3048,2033],{"class":392},[16,3050,3051],{"class":484}," createImageValidator ",[16,3053,396],{"class":373},[16,3055,2514],{"class":537},[16,3057,541],{"class":484},[16,3059,746],{"class":373},[16,3061,3062,3065,3067,3069],{"class":369,"line":460},[16,3063,3064],{"class":377},"  image",[16,3066,754],{"class":373},[16,3068,2509],{"class":537},[16,3070,2623],{"class":484},[16,3072,3073,3076,3078,3080,3082,3084,3087,3090,3092,3094,3096,3098,3100,3102,3104,3106,3108,3110,3112],{"class":369,"line":481},[16,3074,3075],{"class":373},"    .",[16,3077,2631],{"class":537},[16,3079,541],{"class":484},[16,3081,2491],{"class":550},[16,3083,557],{"class":392},[16,3085,3086],{"class":484}," image ",[16,3088,3089],{"class":373},"instanceof",[16,3091,2644],{"class":1299},[16,3093,614],{"class":373},[16,3095,671],{"class":373},[16,3097,1546],{"class":377},[16,3099,754],{"class":373},[16,3101,683],{"class":373},[16,3103,2657],{"class":402},[16,3105,2660],{"class":484},[16,3107,2663],{"class":402},[16,3109,1012],{"class":373},[16,3111,677],{"class":373},[16,3113,584],{"class":484},[16,3115,3116,3118,3120,3122,3124,3126,3128,3130,3133,3135,3137,3139,3141,3143,3145,3147,3149,3151,3153,3155,3157,3159,3161],{"class":369,"line":488},[16,3117,3075],{"class":373},[16,3119,2631],{"class":537},[16,3121,541],{"class":484},[16,3123,2491],{"class":550},[16,3125,557],{"class":392},[16,3127,2581],{"class":484},[16,3129,25],{"class":373},[16,3131,3132],{"class":484},"size ",[16,3134,374],{"class":373},[16,3136,2694],{"class":617},[16,3138,2697],{"class":373},[16,3140,2694],{"class":617},[16,3142,614],{"class":373},[16,3144,671],{"class":373},[16,3146,1546],{"class":377},[16,3148,754],{"class":373},[16,3150,683],{"class":373},[16,3152,2657],{"class":402},[16,3154,2660],{"class":484},[16,3156,2716],{"class":402},[16,3158,1012],{"class":373},[16,3160,677],{"class":373},[16,3162,584],{"class":484},[16,3164,3165,3167,3169,3171,3173,3175,3177,3179,3181,3183,3185,3187,3189,3191,3193,3195,3197,3199,3201,3203,3205,3207,3209,3211,3213,3215],{"class":369,"line":498},[16,3166,3075],{"class":373},[16,3168,2631],{"class":537},[16,3170,541],{"class":484},[16,3172,2491],{"class":550},[16,3174,557],{"class":392},[16,3176,2581],{"class":484},[16,3178,25],{"class":373},[16,3180,2741],{"class":484},[16,3182,25],{"class":373},[16,3184,2746],{"class":537},[16,3186,541],{"class":484},[16,3188,1012],{"class":373},[16,3190,2753],{"class":402},[16,3192,1012],{"class":373},[16,3194,554],{"class":484},[16,3196,614],{"class":373},[16,3198,671],{"class":373},[16,3200,1546],{"class":377},[16,3202,754],{"class":373},[16,3204,683],{"class":373},[16,3206,2657],{"class":402},[16,3208,2660],{"class":484},[16,3210,2774],{"class":402},[16,3212,1012],{"class":373},[16,3214,677],{"class":373},[16,3216,584],{"class":484},[16,3218,3219,3221,3223,3225,3227,3229,3231,3233,3235,3237,3239,3241,3243,3245,3247,3249,3251,3254,3256,3259,3261,3263,3265,3267,3269,3271,3273,3275,3277,3279],{"class":369,"line":508},[16,3220,3075],{"class":373},[16,3222,2631],{"class":537},[16,3224,541],{"class":484},[16,3226,2491],{"class":550},[16,3228,557],{"class":392},[16,3230,2795],{"class":484},[16,3232,1012],{"class":373},[16,3234,2809],{"class":402},[16,3236,1012],{"class":373},[16,3238,614],{"class":373},[16,3240,683],{"class":373},[16,3242,2800],{"class":402},[16,3244,1012],{"class":373},[16,3246,1043],{"class":484},[16,3248,25],{"class":373},[16,3250,2827],{"class":537},[16,3252,3253],{"class":484},"(image",[16,3255,25],{"class":373},[16,3257,3258],{"class":484},"type)",[16,3260,614],{"class":373},[16,3262,671],{"class":373},[16,3264,1546],{"class":377},[16,3266,754],{"class":373},[16,3268,683],{"class":373},[16,3270,2657],{"class":402},[16,3272,2660],{"class":484},[16,3274,2854],{"class":402},[16,3276,1012],{"class":373},[16,3278,677],{"class":373},[16,3280,584],{"class":484},[16,3282,3283,3285],{"class":369,"line":792},[16,3284,625],{"class":373},[16,3286,584],{"class":484},[13,3288,3289],{},"Ensuite, je peux l'utiliser lors de la soumission du formulaire pour éviter d'envoyer des données non valides au serveur :",[359,3291,3293],{"className":361,"code":3292,"language":363,"meta":258,"style":258},"\u003Cscript lang=\"ts\" setup>\nasync function onImageSubmit(event: Event) {\n  const target = event.target as HTMLFormElement\n  const formData = new FormData(target)\n\n  const image = formData.get('image')\n  try {\n    await createImageValidator.parseAsync({ image })\n\n    await $fetch('\u002Fapi\u002Fimage', {\n      method: 'POST',\n      body: formData,\n    })\n  }\n  catch (error) {\n    console.error(error)\n  }\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>Valider les Formulaires avec Nuxt\u003C\u002Fh1>\n\n    \u003Ch2>\n      Image\n    \u003C\u002Fh2>\n    \u003Cform enctype=\"multipart\u002Fform-data\" @submit.prevent=\"onImageSubmit($event)\">\n      \u003Cinput type=\"file\" name=\"image\" accept=\"image\u002Fpng, image\u002Fjpeg, image\u002Fjpg\">\n      \u003Cbutton type=\"submit\">\n        Soumettre\n      \u003C\u002Fbutton>\n    \u003C\u002Fform>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n",[365,3294,3295,3315,3337,3360,3380,3384,3408,3414,3435,3439,3457,3472,3482,3488,3492,3504,3519,3523,3527,3535,3539,3547,3556,3575,3579,3587,3592,3600,3629,3669,3687,3692,3701,3709,3717],{"__ignoreMap":258},[16,3296,3297,3299,3301,3303,3305,3307,3309,3311,3313],{"class":369,"line":370},[16,3298,374],{"class":373},[16,3300,1447],{"class":377},[16,3302,1450],{"class":392},[16,3304,396],{"class":373},[16,3306,399],{"class":373},[16,3308,523],{"class":402},[16,3310,399],{"class":373},[16,3312,1461],{"class":392},[16,3314,381],{"class":373},[16,3316,3317,3319,3321,3324,3326,3328,3330,3333,3335],{"class":369,"line":259},[16,3318,544],{"class":392},[16,3320,1618],{"class":392},[16,3322,3323],{"class":537}," onImageSubmit",[16,3325,541],{"class":373},[16,3327,551],{"class":550},[16,3329,754],{"class":373},[16,3331,3332],{"class":1299}," Event",[16,3334,554],{"class":373},[16,3336,560],{"class":373},[16,3338,3339,3341,3344,3346,3349,3351,3354,3357],{"class":369,"line":422},[16,3340,565],{"class":392},[16,3342,3343],{"class":484}," target",[16,3345,571],{"class":373},[16,3347,3348],{"class":484}," event",[16,3350,25],{"class":373},[16,3352,3353],{"class":484},"target",[16,3355,3356],{"class":530}," as",[16,3358,3359],{"class":1299}," HTMLFormElement\n",[16,3361,3362,3364,3366,3368,3371,3374,3376,3378],{"class":369,"line":460},[16,3363,565],{"class":392},[16,3365,2557],{"class":484},[16,3367,571],{"class":373},[16,3369,3370],{"class":373}," new",[16,3372,3373],{"class":537}," FormData",[16,3375,541],{"class":377},[16,3377,3353],{"class":484},[16,3379,584],{"class":377},[16,3381,3382],{"class":369,"line":481},[16,3383,589],{"emptyLinePlaceholder":268},[16,3385,3386,3388,3390,3392,3394,3396,3398,3400,3402,3404,3406],{"class":369,"line":488},[16,3387,565],{"class":392},[16,3389,2581],{"class":484},[16,3391,571],{"class":373},[16,3393,2557],{"class":484},[16,3395,25],{"class":373},[16,3397,2590],{"class":537},[16,3399,541],{"class":377},[16,3401,1012],{"class":373},[16,3403,2491],{"class":402},[16,3405,1012],{"class":373},[16,3407,584],{"class":377},[16,3409,3410,3412],{"class":369,"line":498},[16,3411,1630],{"class":530},[16,3413,560],{"class":373},[16,3415,3416,3418,3421,3423,3425,3427,3429,3431,3433],{"class":369,"line":508},[16,3417,1660],{"class":530},[16,3419,3420],{"class":484}," createImageValidator",[16,3422,25],{"class":373},[16,3424,1130],{"class":537},[16,3426,541],{"class":377},[16,3428,1543],{"class":373},[16,3430,2581],{"class":484},[16,3432,677],{"class":373},[16,3434,584],{"class":377},[16,3436,3437],{"class":369,"line":792},[16,3438,589],{"emptyLinePlaceholder":268},[16,3440,3441,3443,3445,3447,3449,3451,3453,3455],{"class":369,"line":797},[16,3442,1660],{"class":530},[16,3444,1663],{"class":537},[16,3446,541],{"class":377},[16,3448,1012],{"class":373},[16,3450,2362],{"class":402},[16,3452,1012],{"class":373},[16,3454,614],{"class":373},[16,3456,560],{"class":373},[16,3458,3459,3461,3463,3465,3468,3470],{"class":369,"line":814},[16,3460,1680],{"class":377},[16,3462,754],{"class":373},[16,3464,683],{"class":373},[16,3466,3467],{"class":402},"POST",[16,3469,1012],{"class":373},[16,3471,767],{"class":373},[16,3473,3474,3476,3478,3480],{"class":369,"line":1017},[16,3475,1695],{"class":377},[16,3477,754],{"class":373},[16,3479,2557],{"class":484},[16,3481,767],{"class":373},[16,3483,3484,3486],{"class":369,"line":1053},[16,3485,1056],{"class":373},[16,3487,584],{"class":377},[16,3489,3490],{"class":369,"line":1061},[16,3491,1064],{"class":373},[16,3493,3494,3496,3498,3500,3502],{"class":369,"line":1067},[16,3495,1716],{"class":530},[16,3497,547],{"class":377},[16,3499,1029],{"class":484},[16,3501,971],{"class":377},[16,3503,746],{"class":373},[16,3505,3506,3509,3511,3513,3515,3517],{"class":369,"line":1072},[16,3507,3508],{"class":484},"    console",[16,3510,25],{"class":373},[16,3512,1029],{"class":537},[16,3514,541],{"class":377},[16,3516,1029],{"class":484},[16,3518,584],{"class":377},[16,3520,3521],{"class":369,"line":1088},[16,3522,1064],{"class":373},[16,3524,3525],{"class":369,"line":1093},[16,3526,1816],{"class":373},[16,3528,3529,3531,3533],{"class":369,"line":1098},[16,3530,511],{"class":373},[16,3532,1447],{"class":377},[16,3534,381],{"class":373},[16,3536,3537],{"class":369,"line":1103},[16,3538,589],{"emptyLinePlaceholder":268},[16,3540,3541,3543,3545],{"class":369,"line":1120},[16,3542,374],{"class":373},[16,3544,378],{"class":377},[16,3546,381],{"class":373},[16,3548,3549,3551,3554],{"class":369,"line":1727},[16,3550,386],{"class":373},[16,3552,3553],{"class":377},"div",[16,3555,381],{"class":373},[16,3557,3558,3560,3563,3566,3569,3571,3573],{"class":369,"line":1744},[16,3559,425],{"class":373},[16,3561,3562],{"class":377},"h1",[16,3564,3565],{"class":373},">",[16,3567,3568],{"class":484},"Valider les Formulaires avec Nuxt",[16,3570,511],{"class":373},[16,3572,3562],{"class":377},[16,3574,381],{"class":373},[16,3576,3577],{"class":369,"line":1755},[16,3578,589],{"emptyLinePlaceholder":268},[16,3580,3581,3583,3585],{"class":369,"line":1766},[16,3582,425],{"class":373},[16,3584,103],{"class":377},[16,3586,381],{"class":373},[16,3588,3589],{"class":369,"line":1781},[16,3590,3591],{"class":484},"      Image\n",[16,3593,3594,3596,3598],{"class":369,"line":1796},[16,3595,491],{"class":373},[16,3597,103],{"class":377},[16,3599,381],{"class":373},[16,3601,3602,3604,3606,3608,3610,3612,3614,3616,3618,3620,3622,3625,3627],{"class":369,"line":1803},[16,3603,425],{"class":373},[16,3605,389],{"class":377},[16,3607,2367],{"class":392},[16,3609,396],{"class":373},[16,3611,399],{"class":373},[16,3613,2374],{"class":402},[16,3615,399],{"class":373},[16,3617,1849],{"class":392},[16,3619,396],{"class":373},[16,3621,399],{"class":373},[16,3623,3624],{"class":402},"onImageSubmit($event)",[16,3626,399],{"class":373},[16,3628,381],{"class":373},[16,3630,3631,3634,3636,3638,3640,3642,3644,3646,3649,3651,3653,3655,3657,3659,3661,3663,3665,3667],{"class":369,"line":1808},[16,3632,3633],{"class":373},"      \u003C",[16,3635,428],{"class":377},[16,3637,431],{"class":392},[16,3639,396],{"class":373},[16,3641,399],{"class":373},[16,3643,2403],{"class":402},[16,3645,399],{"class":373},[16,3647,3648],{"class":392}," name",[16,3650,396],{"class":373},[16,3652,399],{"class":373},[16,3654,2491],{"class":402},[16,3656,399],{"class":373},[16,3658,2408],{"class":392},[16,3660,396],{"class":373},[16,3662,399],{"class":373},[16,3664,2415],{"class":402},[16,3666,399],{"class":373},[16,3668,381],{"class":373},[16,3670,3671,3673,3675,3677,3679,3681,3683,3685],{"class":369,"line":1813},[16,3672,3633],{"class":373},[16,3674,465],{"class":377},[16,3676,431],{"class":392},[16,3678,396],{"class":373},[16,3680,399],{"class":373},[16,3682,474],{"class":402},[16,3684,399],{"class":373},[16,3686,381],{"class":373},[16,3688,3689],{"class":369,"line":1819},[16,3690,3691],{"class":484},"        Soumettre\n",[16,3693,3694,3697,3699],{"class":369,"line":1828},[16,3695,3696],{"class":373},"      \u003C\u002F",[16,3698,465],{"class":377},[16,3700,381],{"class":373},[16,3702,3703,3705,3707],{"class":369,"line":1833},[16,3704,491],{"class":373},[16,3706,389],{"class":377},[16,3708,381],{"class":373},[16,3710,3711,3713,3715],{"class":369,"line":1842},[16,3712,501],{"class":373},[16,3714,3553],{"class":377},[16,3716,381],{"class":373},[16,3718,3719,3721,3723],{"class":369,"line":1863},[16,3720,511],{"class":373},[16,3722,378],{"class":377},[16,3724,381],{"class":373},[13,3726,3727,3728,3731],{},"Si la validation échoue, la requête ne sera pas envoyée au serveur et l'erreur sera affichée dans la console. Bien sûr, vous pouvez gérer l'erreur comme vous le souhaitez, comme utiliser un ",[365,3729,3730],{},"toast"," ou afficher un message d'erreur sous l'entrée concernée.",[13,3733,3734],{},"Côté serveur, je peux remplacer la logique de validation par le validateur :",[359,3736,3738],{"className":521,"code":3737,"language":523,"meta":258,"style":258},"import { saveImageValidator } from '~\u002Futils\u002Fvalidators'\n\nexport default defineEventHandler(async (event) => {\n  \u002F\u002F ...\n\n  await saveImageValidator.parseAsync({ image })\n\n  \u002F\u002F ...\n})\n",[365,3739,3740,3759,3763,3785,3790,3794,3814,3818,3822],{"__ignoreMap":258},[16,3741,3742,3744,3746,3749,3751,3753,3755,3757],{"class":369,"line":370},[16,3743,668],{"class":530},[16,3745,671],{"class":373},[16,3747,3748],{"class":484}," saveImageValidator",[16,3750,677],{"class":373},[16,3752,680],{"class":530},[16,3754,683],{"class":373},[16,3756,2220],{"class":402},[16,3758,689],{"class":373},[16,3760,3761],{"class":369,"line":259},[16,3762,589],{"emptyLinePlaceholder":268},[16,3764,3765,3767,3769,3771,3773,3775,3777,3779,3781,3783],{"class":369,"line":422},[16,3766,531],{"class":530},[16,3768,534],{"class":530},[16,3770,538],{"class":537},[16,3772,541],{"class":484},[16,3774,544],{"class":392},[16,3776,547],{"class":373},[16,3778,551],{"class":550},[16,3780,554],{"class":373},[16,3782,557],{"class":392},[16,3784,560],{"class":373},[16,3786,3787],{"class":369,"line":460},[16,3788,3789],{"class":594},"  \u002F\u002F ...\n",[16,3791,3792],{"class":369,"line":481},[16,3793,589],{"emptyLinePlaceholder":268},[16,3795,3796,3798,3800,3802,3804,3806,3808,3810,3812],{"class":369,"line":488},[16,3797,2605],{"class":530},[16,3799,3748],{"class":484},[16,3801,25],{"class":373},[16,3803,1130],{"class":537},[16,3805,541],{"class":377},[16,3807,1543],{"class":373},[16,3809,2581],{"class":484},[16,3811,677],{"class":373},[16,3813,584],{"class":377},[16,3815,3816],{"class":369,"line":498},[16,3817,589],{"emptyLinePlaceholder":268},[16,3819,3820],{"class":369,"line":508},[16,3821,3789],{"class":594},[16,3823,3824,3826],{"class":369,"line":792},[16,3825,625],{"class":373},[16,3827,584],{"class":484},[640,3829,3831],{"id":3830},"avec-nuxthub","Avec NuxtHub",[13,3833,3834,3835,3837,3838],{},"NuxtHub est livré avec un utilitaire nommé ",[365,3836,2321],{}," qui peut être utilisé pour vérifier la taille et le type du fichier. Cet utilitaire est très utile car il lancera automatiquement une erreur si le fichier n'est pas valide, mais ne peut être utilisé que côté serveur. Cela signifie que j'aurai un schéma Zod côté client et un utilitaire NuxtHub côté serveur. ",[67,3839,3840],{},"Utilisez des constantes pour partager les limites de validation entre le schéma Zod et l'utilitaire NuxtHub.",[359,3842,3844],{"className":521,"code":3843,"language":523,"meta":258,"style":258},"export default defineEventHandler(async (event) => {\n  const formData = await readFormData(event)\n\n  const image = formData.get('image') as Blob\n  ensureBlob(image, {\n    maxSize: '1Mo',\n    types: ['image\u002Fpng', 'image\u002Fjpeg', 'image\u002Fjpg']\n  })\n\n  \u002F\u002F Enregistrer l'image dans une base de données ou un système de fichiers\n\n  return sendNoContent(event, 201)\n})\n",[365,3845,3846,3868,3886,3890,3920,3933,3949,3983,3989,3993,3997,4001,4017],{"__ignoreMap":258},[16,3847,3848,3850,3852,3854,3856,3858,3860,3862,3864,3866],{"class":369,"line":370},[16,3849,531],{"class":530},[16,3851,534],{"class":530},[16,3853,538],{"class":537},[16,3855,541],{"class":484},[16,3857,544],{"class":392},[16,3859,547],{"class":373},[16,3861,551],{"class":550},[16,3863,554],{"class":373},[16,3865,557],{"class":392},[16,3867,560],{"class":373},[16,3869,3870,3872,3874,3876,3878,3880,3882,3884],{"class":369,"line":259},[16,3871,565],{"class":392},[16,3873,2557],{"class":484},[16,3875,571],{"class":373},[16,3877,574],{"class":530},[16,3879,2564],{"class":537},[16,3881,541],{"class":377},[16,3883,551],{"class":484},[16,3885,584],{"class":377},[16,3887,3888],{"class":369,"line":422},[16,3889,589],{"emptyLinePlaceholder":268},[16,3891,3892,3894,3896,3898,3900,3902,3904,3906,3908,3910,3912,3914,3917],{"class":369,"line":460},[16,3893,565],{"class":392},[16,3895,2581],{"class":484},[16,3897,571],{"class":373},[16,3899,2557],{"class":484},[16,3901,25],{"class":373},[16,3903,2590],{"class":537},[16,3905,541],{"class":377},[16,3907,1012],{"class":373},[16,3909,2491],{"class":402},[16,3911,1012],{"class":373},[16,3913,971],{"class":377},[16,3915,3916],{"class":530},"as",[16,3918,3919],{"class":1299}," Blob\n",[16,3921,3922,3925,3927,3929,3931],{"class":369,"line":481},[16,3923,3924],{"class":537},"  ensureBlob",[16,3926,541],{"class":377},[16,3928,2491],{"class":484},[16,3930,614],{"class":373},[16,3932,560],{"class":373},[16,3934,3935,3938,3940,3942,3945,3947],{"class":369,"line":488},[16,3936,3937],{"class":377},"    maxSize",[16,3939,754],{"class":373},[16,3941,683],{"class":373},[16,3943,3944],{"class":402},"1Mo",[16,3946,1012],{"class":373},[16,3948,767],{"class":373},[16,3950,3951,3954,3956,3958,3960,3962,3964,3966,3968,3970,3972,3974,3976,3978,3980],{"class":369,"line":498},[16,3952,3953],{"class":377},"    types",[16,3955,754],{"class":373},[16,3957,2795],{"class":377},[16,3959,1012],{"class":373},[16,3961,2800],{"class":402},[16,3963,1012],{"class":373},[16,3965,614],{"class":373},[16,3967,683],{"class":373},[16,3969,2809],{"class":402},[16,3971,1012],{"class":373},[16,3973,614],{"class":373},[16,3975,683],{"class":373},[16,3977,2818],{"class":402},[16,3979,1012],{"class":373},[16,3981,3982],{"class":377},"]\n",[16,3984,3985,3987],{"class":369,"line":508},[16,3986,772],{"class":373},[16,3988,584],{"class":377},[16,3990,3991],{"class":369,"line":792},[16,3992,589],{"emptyLinePlaceholder":268},[16,3994,3995],{"class":369,"line":797},[16,3996,2975],{"class":594},[16,3998,3999],{"class":369,"line":814},[16,4000,589],{"emptyLinePlaceholder":268},[16,4002,4003,4005,4007,4009,4011,4013,4015],{"class":369,"line":1017},[16,4004,604],{"class":530},[16,4006,607],{"class":537},[16,4008,541],{"class":377},[16,4010,551],{"class":484},[16,4012,614],{"class":373},[16,4014,618],{"class":617},[16,4016,584],{"class":377},[16,4018,4019,4021],{"class":369,"line":1053},[16,4020,625],{"class":373},[16,4022,584],{"class":484},[103,4024,4026],{"id":4025},"texte-et-blob-dans-le-même-formulaire","Texte et Blob dans le Même Formulaire",[13,4028,4029],{},"Jusqu'à présent, j'ai seulement un type d'entrée dans mon formulaire, texte ou fichier. Mais que se passe-t-il si je veux avoir les deux dans le même formulaire ?",[13,4031,4032,4033,4035],{},"Vous devez savoir que vous devez absolument utiliser un ",[365,4034,2473],{}," pour envoyer le formulaire au serveur en raison de l'entrée de fichier.",[13,4037,4038],{},"Le formulaire est toujours très simple :",[359,4040,4042],{"className":361,"code":4041,"language":363,"meta":258,"style":258},"\u003Ctemplate>\n  \u003Cform action=\"\u002Fapi\u002Ftext-image\" enctype=\"multipart\u002Fform-data\" method=\"post\">\n    \u003Cinput type=\"text\" name=\"name\" placeholder=\"Nom\" required>\n    \u003Cinput type=\"file\" name=\"image\" accept=\"image\u002Fpng, image\u002Fjpeg, image\u002Fjpg\" required>\n    \u003Cbutton type=\"submit\">\n      Créer Tag\n    \u003C\u002Fbutton>\n  \u003C\u002Fform>\n\u003C\u002Ftemplate>\n",[365,4043,4044,4052,4091,4132,4172,4190,4194,4202,4210],{"__ignoreMap":258},[16,4045,4046,4048,4050],{"class":369,"line":370},[16,4047,374],{"class":373},[16,4049,378],{"class":377},[16,4051,381],{"class":373},[16,4053,4054,4056,4058,4060,4062,4064,4067,4069,4071,4073,4075,4077,4079,4081,4083,4085,4087,4089],{"class":369,"line":259},[16,4055,386],{"class":373},[16,4057,389],{"class":377},[16,4059,408],{"class":392},[16,4061,396],{"class":373},[16,4063,399],{"class":373},[16,4065,4066],{"class":402},"\u002Fapi\u002Ftext-image",[16,4068,399],{"class":373},[16,4070,2367],{"class":392},[16,4072,396],{"class":373},[16,4074,399],{"class":373},[16,4076,2374],{"class":402},[16,4078,399],{"class":373},[16,4080,393],{"class":392},[16,4082,396],{"class":373},[16,4084,399],{"class":373},[16,4086,403],{"class":402},[16,4088,399],{"class":373},[16,4090,381],{"class":373},[16,4092,4093,4095,4097,4099,4101,4103,4105,4107,4109,4111,4113,4116,4118,4120,4122,4124,4126,4128,4130],{"class":369,"line":422},[16,4094,425],{"class":373},[16,4096,428],{"class":377},[16,4098,431],{"class":392},[16,4100,396],{"class":373},[16,4102,399],{"class":373},[16,4104,438],{"class":402},[16,4106,399],{"class":373},[16,4108,3648],{"class":392},[16,4110,396],{"class":373},[16,4112,399],{"class":373},[16,4114,4115],{"class":402},"name",[16,4117,399],{"class":373},[16,4119,443],{"class":392},[16,4121,396],{"class":373},[16,4123,399],{"class":373},[16,4125,450],{"class":402},[16,4127,399],{"class":373},[16,4129,455],{"class":392},[16,4131,381],{"class":373},[16,4133,4134,4136,4138,4140,4142,4144,4146,4148,4150,4152,4154,4156,4158,4160,4162,4164,4166,4168,4170],{"class":369,"line":460},[16,4135,425],{"class":373},[16,4137,428],{"class":377},[16,4139,431],{"class":392},[16,4141,396],{"class":373},[16,4143,399],{"class":373},[16,4145,2403],{"class":402},[16,4147,399],{"class":373},[16,4149,3648],{"class":392},[16,4151,396],{"class":373},[16,4153,399],{"class":373},[16,4155,2491],{"class":402},[16,4157,399],{"class":373},[16,4159,2408],{"class":392},[16,4161,396],{"class":373},[16,4163,399],{"class":373},[16,4165,2415],{"class":402},[16,4167,399],{"class":373},[16,4169,455],{"class":392},[16,4171,381],{"class":373},[16,4173,4174,4176,4178,4180,4182,4184,4186,4188],{"class":369,"line":481},[16,4175,425],{"class":373},[16,4177,465],{"class":377},[16,4179,431],{"class":392},[16,4181,396],{"class":373},[16,4183,399],{"class":373},[16,4185,474],{"class":402},[16,4187,399],{"class":373},[16,4189,381],{"class":373},[16,4191,4192],{"class":369,"line":488},[16,4193,485],{"class":484},[16,4195,4196,4198,4200],{"class":369,"line":498},[16,4197,491],{"class":373},[16,4199,465],{"class":377},[16,4201,381],{"class":373},[16,4203,4204,4206,4208],{"class":369,"line":508},[16,4205,501],{"class":373},[16,4207,389],{"class":377},[16,4209,381],{"class":373},[16,4211,4212,4214,4216],{"class":369,"line":792},[16,4213,511],{"class":373},[16,4215,378],{"class":377},[16,4217,381],{"class":373},[13,4219,4220],{},"Maintenant, je peux créer un validateur en utilisant Zod :",[359,4222,4224],{"className":521,"code":4223,"language":523,"meta":258,"style":258},"import { any, object, string } from 'zod'\n\nexport const createValidator = object({\n  name: string({ message: 'Obligatoire' }),\n  image: any().refine(image => image instanceof File, { message: 'L\\'image doit être un fichier' }).refine(image => image.size \u003C 1024 * 1024, { message: 'L\\'image doit faire moins de 1 Mo' }).refine(image => image.type.startsWith('image\u002F'), { message: 'L\\'image doit être une image' }).refine(image => ['image\u002Fpng', 'image\u002Fjpeg', 'image\u002Fjpg'].includes(image.type), { message: 'L\\'image doit être un JPEG ou un PNG' })\n})\n",[365,4225,4226,4253,4257,4274,4302,4516],{"__ignoreMap":258},[16,4227,4228,4230,4232,4234,4236,4238,4240,4243,4245,4247,4249,4251],{"class":369,"line":370},[16,4229,668],{"class":530},[16,4231,671],{"class":373},[16,4233,2509],{"class":484},[16,4235,614],{"class":373},[16,4237,2514],{"class":484},[16,4239,614],{"class":373},[16,4241,4242],{"class":484}," string",[16,4244,677],{"class":373},[16,4246,680],{"class":530},[16,4248,683],{"class":373},[16,4250,686],{"class":402},[16,4252,689],{"class":373},[16,4254,4255],{"class":369,"line":259},[16,4256,589],{"emptyLinePlaceholder":268},[16,4258,4259,4261,4263,4266,4268,4270,4272],{"class":369,"line":422},[16,4260,531],{"class":530},[16,4262,2033],{"class":392},[16,4264,4265],{"class":484}," createValidator ",[16,4267,396],{"class":373},[16,4269,2514],{"class":537},[16,4271,541],{"class":484},[16,4273,746],{"class":373},[16,4275,4276,4278,4280,4282,4284,4286,4288,4290,4292,4294,4296,4298,4300],{"class":369,"line":460},[16,4277,1530],{"class":377},[16,4279,754],{"class":373},[16,4281,4242],{"class":537},[16,4283,541],{"class":484},[16,4285,1543],{"class":373},[16,4287,1546],{"class":377},[16,4289,754],{"class":373},[16,4291,683],{"class":373},[16,4293,1553],{"class":402},[16,4295,1012],{"class":373},[16,4297,677],{"class":373},[16,4299,554],{"class":484},[16,4301,767],{"class":373},[16,4303,4304,4306,4308,4310,4312,4314,4316,4318,4320,4322,4324,4326,4328,4330,4332,4334,4336,4338,4340,4342,4344,4346,4348,4350,4352,4354,4356,4358,4360,4362,4364,4366,4368,4370,4372,4374,4376,4378,4380,4382,4384,4386,4388,4390,4392,4394,4396,4398,4400,4402,4404,4406,4408,4410,4412,4414,4416,4418,4420,4422,4424,4426,4428,4430,4432,4434,4436,4438,4440,4442,4444,4446,4448,4450,4452,4454,4456,4458,4460,4462,4464,4466,4468,4470,4472,4474,4476,4478,4480,4482,4484,4486,4488,4490,4492,4494,4496,4498,4500,4502,4504,4506,4508,4510,4512,4514],{"class":369,"line":481},[16,4305,3064],{"class":377},[16,4307,754],{"class":373},[16,4309,2509],{"class":537},[16,4311,764],{"class":484},[16,4313,25],{"class":373},[16,4315,2631],{"class":537},[16,4317,541],{"class":484},[16,4319,2491],{"class":550},[16,4321,557],{"class":392},[16,4323,3086],{"class":484},[16,4325,3089],{"class":373},[16,4327,2644],{"class":1299},[16,4329,614],{"class":373},[16,4331,671],{"class":373},[16,4333,1546],{"class":377},[16,4335,754],{"class":373},[16,4337,683],{"class":373},[16,4339,2657],{"class":402},[16,4341,2660],{"class":484},[16,4343,2663],{"class":402},[16,4345,1012],{"class":373},[16,4347,677],{"class":373},[16,4349,554],{"class":484},[16,4351,25],{"class":373},[16,4353,2631],{"class":537},[16,4355,541],{"class":484},[16,4357,2491],{"class":550},[16,4359,557],{"class":392},[16,4361,2581],{"class":484},[16,4363,25],{"class":373},[16,4365,3132],{"class":484},[16,4367,374],{"class":373},[16,4369,2694],{"class":617},[16,4371,2697],{"class":373},[16,4373,2694],{"class":617},[16,4375,614],{"class":373},[16,4377,671],{"class":373},[16,4379,1546],{"class":377},[16,4381,754],{"class":373},[16,4383,683],{"class":373},[16,4385,2657],{"class":402},[16,4387,2660],{"class":484},[16,4389,2716],{"class":402},[16,4391,1012],{"class":373},[16,4393,677],{"class":373},[16,4395,554],{"class":484},[16,4397,25],{"class":373},[16,4399,2631],{"class":537},[16,4401,541],{"class":484},[16,4403,2491],{"class":550},[16,4405,557],{"class":392},[16,4407,2581],{"class":484},[16,4409,25],{"class":373},[16,4411,2741],{"class":484},[16,4413,25],{"class":373},[16,4415,2746],{"class":537},[16,4417,541],{"class":484},[16,4419,1012],{"class":373},[16,4421,2753],{"class":402},[16,4423,1012],{"class":373},[16,4425,554],{"class":484},[16,4427,614],{"class":373},[16,4429,671],{"class":373},[16,4431,1546],{"class":377},[16,4433,754],{"class":373},[16,4435,683],{"class":373},[16,4437,2657],{"class":402},[16,4439,2660],{"class":484},[16,4441,2774],{"class":402},[16,4443,1012],{"class":373},[16,4445,677],{"class":373},[16,4447,554],{"class":484},[16,4449,25],{"class":373},[16,4451,2631],{"class":537},[16,4453,541],{"class":484},[16,4455,2491],{"class":550},[16,4457,557],{"class":392},[16,4459,2795],{"class":484},[16,4461,1012],{"class":373},[16,4463,2800],{"class":402},[16,4465,1012],{"class":373},[16,4467,614],{"class":373},[16,4469,683],{"class":373},[16,4471,2809],{"class":402},[16,4473,1012],{"class":373},[16,4475,614],{"class":373},[16,4477,683],{"class":373},[16,4479,2818],{"class":402},[16,4481,1012],{"class":373},[16,4483,1043],{"class":484},[16,4485,25],{"class":373},[16,4487,2827],{"class":537},[16,4489,3253],{"class":484},[16,4491,25],{"class":373},[16,4493,3258],{"class":484},[16,4495,614],{"class":373},[16,4497,671],{"class":373},[16,4499,1546],{"class":377},[16,4501,754],{"class":373},[16,4503,683],{"class":373},[16,4505,2657],{"class":402},[16,4507,2660],{"class":484},[16,4509,2854],{"class":402},[16,4511,1012],{"class":373},[16,4513,677],{"class":373},[16,4515,584],{"class":484},[16,4517,4518,4520],{"class":369,"line":488},[16,4519,625],{"class":373},[16,4521,584],{"class":484},[13,4523,4524],{},"Ce validateur peut être utilisé comme auparavant, sur le serveur et le client.",[13,4526,4527,4528,4530],{},"Si vous préférez utiliser l'utilitaire ",[365,4529,2321],{}," de NuxtHub, je vous recommande de diviser le validateur en deux parties : une pour le texte et une pour l'image. Cela signifie que le texte et l'image seront utilisés côté client en les fusionnant en un seul objet, et uniquement le texte sera utilisé côté serveur.",[359,4532,4534],{"className":521,"code":4533,"language":523,"meta":258,"style":258},"import { any, object, string } from 'zod'\n\nexport const createTextValidator = object({\n  name: string({ message: 'Obligatoire' }),\n})\n\nexport const createImageValidator = object({\n  image: any().refine(image => image instanceof File, { message: 'L\\'image doit être un fichier' }).refine(image => image.size \u003C 1024 * 1024, { message: 'L\\'image doit faire moins de 1 Mo' }).refine(image => image.type.startsWith('image\u002F'), { message: 'L\\'image doit être une image' }).refine(image => ['image\u002Fpng', 'image\u002Fjpeg', 'image\u002Fjpg'].includes(image.type), { message: 'L\\'image doit être un JPEG ou un PNG' })\n})\n\nexport const createValidator = createTextValidator.merge(createImageValidator)\n",[365,4535,4536,4562,4566,4583,4611,4617,4621,4637,4851,4857,4861],{"__ignoreMap":258},[16,4537,4538,4540,4542,4544,4546,4548,4550,4552,4554,4556,4558,4560],{"class":369,"line":370},[16,4539,668],{"class":530},[16,4541,671],{"class":373},[16,4543,2509],{"class":484},[16,4545,614],{"class":373},[16,4547,2514],{"class":484},[16,4549,614],{"class":373},[16,4551,4242],{"class":484},[16,4553,677],{"class":373},[16,4555,680],{"class":530},[16,4557,683],{"class":373},[16,4559,686],{"class":402},[16,4561,689],{"class":373},[16,4563,4564],{"class":369,"line":259},[16,4565,589],{"emptyLinePlaceholder":268},[16,4567,4568,4570,4572,4575,4577,4579,4581],{"class":369,"line":422},[16,4569,531],{"class":530},[16,4571,2033],{"class":392},[16,4573,4574],{"class":484}," createTextValidator ",[16,4576,396],{"class":373},[16,4578,2514],{"class":537},[16,4580,541],{"class":484},[16,4582,746],{"class":373},[16,4584,4585,4587,4589,4591,4593,4595,4597,4599,4601,4603,4605,4607,4609],{"class":369,"line":460},[16,4586,1530],{"class":377},[16,4588,754],{"class":373},[16,4590,4242],{"class":537},[16,4592,541],{"class":484},[16,4594,1543],{"class":373},[16,4596,1546],{"class":377},[16,4598,754],{"class":373},[16,4600,683],{"class":373},[16,4602,1553],{"class":402},[16,4604,1012],{"class":373},[16,4606,677],{"class":373},[16,4608,554],{"class":484},[16,4610,767],{"class":373},[16,4612,4613,4615],{"class":369,"line":481},[16,4614,625],{"class":373},[16,4616,584],{"class":484},[16,4618,4619],{"class":369,"line":488},[16,4620,589],{"emptyLinePlaceholder":268},[16,4622,4623,4625,4627,4629,4631,4633,4635],{"class":369,"line":498},[16,4624,531],{"class":530},[16,4626,2033],{"class":392},[16,4628,3051],{"class":484},[16,4630,396],{"class":373},[16,4632,2514],{"class":537},[16,4634,541],{"class":484},[16,4636,746],{"class":373},[16,4638,4639,4641,4643,4645,4647,4649,4651,4653,4655,4657,4659,4661,4663,4665,4667,4669,4671,4673,4675,4677,4679,4681,4683,4685,4687,4689,4691,4693,4695,4697,4699,4701,4703,4705,4707,4709,4711,4713,4715,4717,4719,4721,4723,4725,4727,4729,4731,4733,4735,4737,4739,4741,4743,4745,4747,4749,4751,4753,4755,4757,4759,4761,4763,4765,4767,4769,4771,4773,4775,4777,4779,4781,4783,4785,4787,4789,4791,4793,4795,4797,4799,4801,4803,4805,4807,4809,4811,4813,4815,4817,4819,4821,4823,4825,4827,4829,4831,4833,4835,4837,4839,4841,4843,4845,4847,4849],{"class":369,"line":508},[16,4640,3064],{"class":377},[16,4642,754],{"class":373},[16,4644,2509],{"class":537},[16,4646,764],{"class":484},[16,4648,25],{"class":373},[16,4650,2631],{"class":537},[16,4652,541],{"class":484},[16,4654,2491],{"class":550},[16,4656,557],{"class":392},[16,4658,3086],{"class":484},[16,4660,3089],{"class":373},[16,4662,2644],{"class":1299},[16,4664,614],{"class":373},[16,4666,671],{"class":373},[16,4668,1546],{"class":377},[16,4670,754],{"class":373},[16,4672,683],{"class":373},[16,4674,2657],{"class":402},[16,4676,2660],{"class":484},[16,4678,2663],{"class":402},[16,4680,1012],{"class":373},[16,4682,677],{"class":373},[16,4684,554],{"class":484},[16,4686,25],{"class":373},[16,4688,2631],{"class":537},[16,4690,541],{"class":484},[16,4692,2491],{"class":550},[16,4694,557],{"class":392},[16,4696,2581],{"class":484},[16,4698,25],{"class":373},[16,4700,3132],{"class":484},[16,4702,374],{"class":373},[16,4704,2694],{"class":617},[16,4706,2697],{"class":373},[16,4708,2694],{"class":617},[16,4710,614],{"class":373},[16,4712,671],{"class":373},[16,4714,1546],{"class":377},[16,4716,754],{"class":373},[16,4718,683],{"class":373},[16,4720,2657],{"class":402},[16,4722,2660],{"class":484},[16,4724,2716],{"class":402},[16,4726,1012],{"class":373},[16,4728,677],{"class":373},[16,4730,554],{"class":484},[16,4732,25],{"class":373},[16,4734,2631],{"class":537},[16,4736,541],{"class":484},[16,4738,2491],{"class":550},[16,4740,557],{"class":392},[16,4742,2581],{"class":484},[16,4744,25],{"class":373},[16,4746,2741],{"class":484},[16,4748,25],{"class":373},[16,4750,2746],{"class":537},[16,4752,541],{"class":484},[16,4754,1012],{"class":373},[16,4756,2753],{"class":402},[16,4758,1012],{"class":373},[16,4760,554],{"class":484},[16,4762,614],{"class":373},[16,4764,671],{"class":373},[16,4766,1546],{"class":377},[16,4768,754],{"class":373},[16,4770,683],{"class":373},[16,4772,2657],{"class":402},[16,4774,2660],{"class":484},[16,4776,2774],{"class":402},[16,4778,1012],{"class":373},[16,4780,677],{"class":373},[16,4782,554],{"class":484},[16,4784,25],{"class":373},[16,4786,2631],{"class":537},[16,4788,541],{"class":484},[16,4790,2491],{"class":550},[16,4792,557],{"class":392},[16,4794,2795],{"class":484},[16,4796,1012],{"class":373},[16,4798,2800],{"class":402},[16,4800,1012],{"class":373},[16,4802,614],{"class":373},[16,4804,683],{"class":373},[16,4806,2809],{"class":402},[16,4808,1012],{"class":373},[16,4810,614],{"class":373},[16,4812,683],{"class":373},[16,4814,2818],{"class":402},[16,4816,1012],{"class":373},[16,4818,1043],{"class":484},[16,4820,25],{"class":373},[16,4822,2827],{"class":537},[16,4824,3253],{"class":484},[16,4826,25],{"class":373},[16,4828,3258],{"class":484},[16,4830,614],{"class":373},[16,4832,671],{"class":373},[16,4834,1546],{"class":377},[16,4836,754],{"class":373},[16,4838,683],{"class":373},[16,4840,2657],{"class":402},[16,4842,2660],{"class":484},[16,4844,2854],{"class":402},[16,4846,1012],{"class":373},[16,4848,677],{"class":373},[16,4850,584],{"class":484},[16,4852,4853,4855],{"class":369,"line":792},[16,4854,625],{"class":373},[16,4856,584],{"class":484},[16,4858,4859],{"class":369,"line":797},[16,4860,589],{"emptyLinePlaceholder":268},[16,4862,4863,4865,4867,4869,4871,4874,4876,4879],{"class":369,"line":814},[16,4864,531],{"class":530},[16,4866,2033],{"class":392},[16,4868,4265],{"class":484},[16,4870,396],{"class":373},[16,4872,4873],{"class":484}," createTextValidator",[16,4875,25],{"class":373},[16,4877,4878],{"class":537},"merge",[16,4880,4881],{"class":484},"(createImageValidator)\n",[13,4883,4884,4885,4888],{},"Côté client, j'utiliserai le ",[365,4886,4887],{},"createValidator"," pour valider le formulaire avant de l'envoyer au serveur :",[359,4890,4892],{"className":361,"code":4891,"language":363,"meta":258,"style":258},"\u003Cscript lang=\"ts\" setup>\nasync function onSubmit(event: Event) {\n  const target = event.target as HTMLFormElement\n  const formData = new FormData(target)\n\n  const name = formData.get('name')\n  const image = formData.get('image')\n  try {\n    await createValidator.parseAsync({ name, image })\n\n    await $fetch('\u002Fapi\u002Ftext-image', {\n      method: 'POST',\n      body: formData,\n    })\n  }\n  catch (error) {\n    console.error(error)\n  }\n}\n\u003C\u002Fscript>\n",[365,4893,4894,4914,4934,4952,4970,4974,4998,5022,5028,5053,5057,5075,5089,5099,5105,5109,5121,5135,5139,5143],{"__ignoreMap":258},[16,4895,4896,4898,4900,4902,4904,4906,4908,4910,4912],{"class":369,"line":370},[16,4897,374],{"class":373},[16,4899,1447],{"class":377},[16,4901,1450],{"class":392},[16,4903,396],{"class":373},[16,4905,399],{"class":373},[16,4907,523],{"class":402},[16,4909,399],{"class":373},[16,4911,1461],{"class":392},[16,4913,381],{"class":373},[16,4915,4916,4918,4920,4922,4924,4926,4928,4930,4932],{"class":369,"line":259},[16,4917,544],{"class":392},[16,4919,1618],{"class":392},[16,4921,1621],{"class":537},[16,4923,541],{"class":373},[16,4925,551],{"class":550},[16,4927,754],{"class":373},[16,4929,3332],{"class":1299},[16,4931,554],{"class":373},[16,4933,560],{"class":373},[16,4935,4936,4938,4940,4942,4944,4946,4948,4950],{"class":369,"line":422},[16,4937,565],{"class":392},[16,4939,3343],{"class":484},[16,4941,571],{"class":373},[16,4943,3348],{"class":484},[16,4945,25],{"class":373},[16,4947,3353],{"class":484},[16,4949,3356],{"class":530},[16,4951,3359],{"class":1299},[16,4953,4954,4956,4958,4960,4962,4964,4966,4968],{"class":369,"line":460},[16,4955,565],{"class":392},[16,4957,2557],{"class":484},[16,4959,571],{"class":373},[16,4961,3370],{"class":373},[16,4963,3373],{"class":537},[16,4965,541],{"class":377},[16,4967,3353],{"class":484},[16,4969,584],{"class":377},[16,4971,4972],{"class":369,"line":481},[16,4973,589],{"emptyLinePlaceholder":268},[16,4975,4976,4978,4980,4982,4984,4986,4988,4990,4992,4994,4996],{"class":369,"line":488},[16,4977,565],{"class":392},[16,4979,3648],{"class":484},[16,4981,571],{"class":373},[16,4983,2557],{"class":484},[16,4985,25],{"class":373},[16,4987,2590],{"class":537},[16,4989,541],{"class":377},[16,4991,1012],{"class":373},[16,4993,4115],{"class":402},[16,4995,1012],{"class":373},[16,4997,584],{"class":377},[16,4999,5000,5002,5004,5006,5008,5010,5012,5014,5016,5018,5020],{"class":369,"line":498},[16,5001,565],{"class":392},[16,5003,2581],{"class":484},[16,5005,571],{"class":373},[16,5007,2557],{"class":484},[16,5009,25],{"class":373},[16,5011,2590],{"class":537},[16,5013,541],{"class":377},[16,5015,1012],{"class":373},[16,5017,2491],{"class":402},[16,5019,1012],{"class":373},[16,5021,584],{"class":377},[16,5023,5024,5026],{"class":369,"line":508},[16,5025,1630],{"class":530},[16,5027,560],{"class":373},[16,5029,5030,5032,5035,5037,5039,5041,5043,5045,5047,5049,5051],{"class":369,"line":792},[16,5031,1660],{"class":530},[16,5033,5034],{"class":484}," createValidator",[16,5036,25],{"class":373},[16,5038,1130],{"class":537},[16,5040,541],{"class":377},[16,5042,1543],{"class":373},[16,5044,3648],{"class":484},[16,5046,614],{"class":373},[16,5048,2581],{"class":484},[16,5050,677],{"class":373},[16,5052,584],{"class":377},[16,5054,5055],{"class":369,"line":797},[16,5056,589],{"emptyLinePlaceholder":268},[16,5058,5059,5061,5063,5065,5067,5069,5071,5073],{"class":369,"line":814},[16,5060,1660],{"class":530},[16,5062,1663],{"class":537},[16,5064,541],{"class":377},[16,5066,1012],{"class":373},[16,5068,4066],{"class":402},[16,5070,1012],{"class":373},[16,5072,614],{"class":373},[16,5074,560],{"class":373},[16,5076,5077,5079,5081,5083,5085,5087],{"class":369,"line":1017},[16,5078,1680],{"class":377},[16,5080,754],{"class":373},[16,5082,683],{"class":373},[16,5084,3467],{"class":402},[16,5086,1012],{"class":373},[16,5088,767],{"class":373},[16,5090,5091,5093,5095,5097],{"class":369,"line":1053},[16,5092,1695],{"class":377},[16,5094,754],{"class":373},[16,5096,2557],{"class":484},[16,5098,767],{"class":373},[16,5100,5101,5103],{"class":369,"line":1061},[16,5102,1056],{"class":373},[16,5104,584],{"class":377},[16,5106,5107],{"class":369,"line":1067},[16,5108,1064],{"class":373},[16,5110,5111,5113,5115,5117,5119],{"class":369,"line":1072},[16,5112,1716],{"class":530},[16,5114,547],{"class":377},[16,5116,1029],{"class":484},[16,5118,971],{"class":377},[16,5120,746],{"class":373},[16,5122,5123,5125,5127,5129,5131,5133],{"class":369,"line":1088},[16,5124,3508],{"class":484},[16,5126,25],{"class":373},[16,5128,1029],{"class":537},[16,5130,541],{"class":377},[16,5132,1029],{"class":484},[16,5134,584],{"class":377},[16,5136,5137],{"class":369,"line":1093},[16,5138,1064],{"class":373},[16,5140,5141],{"class":369,"line":1098},[16,5142,1816],{"class":373},[16,5144,5145,5147,5149],{"class":369,"line":1103},[16,5146,511],{"class":373},[16,5148,1447],{"class":377},[16,5150,381],{"class":373},[13,5152,5153,5154,5157,5158,5160],{},"Côté serveur, j'utiliserai le ",[365,5155,5156],{},"createTextValidator"," pour valider le texte et l'utilitaire ",[365,5159,2321],{}," pour valider l'image :",[359,5162,5164],{"className":521,"code":5163,"language":523,"meta":258,"style":258},"import { createTextValidator } from '~\u002Futils\u002Fvalidators'\n\nexport default defineEventHandler(async (event) => {\n  const formData = await readFormData(event)\n\n  const name = formData.get('name')\n  const data = await createTextValidator.parseAsync({ name }).catch((error) => {\n    throw createError({\n      status: 400,\n      message: 'Données non valides',\n      data: error.errors[0].message\n    })\n  })\n\n  \u002F\u002F Enregistrer le texte dans la base de données...\n\n  const image = formData.get('image') as Blob\n  ensureBlob(image, {\n    maxSize: '1 Mo',\n    types: ['image\u002Fpng', 'image\u002Fjpeg', 'image\u002Fjpg']\n  })\n\n  \u002F\u002F Enregistrer l'image dans une base de données ou un système de fichiers\n\n  return sendNoContent(event, 201)\n})\n",[365,5165,5166,5184,5188,5210,5228,5232,5256,5298,5308,5318,5332,5354,5360,5366,5370,5375,5379,5407,5419,5434,5466,5472,5476,5480,5484,5500],{"__ignoreMap":258},[16,5167,5168,5170,5172,5174,5176,5178,5180,5182],{"class":369,"line":370},[16,5169,668],{"class":530},[16,5171,671],{"class":373},[16,5173,4873],{"class":484},[16,5175,677],{"class":373},[16,5177,680],{"class":530},[16,5179,683],{"class":373},[16,5181,2220],{"class":402},[16,5183,689],{"class":373},[16,5185,5186],{"class":369,"line":259},[16,5187,589],{"emptyLinePlaceholder":268},[16,5189,5190,5192,5194,5196,5198,5200,5202,5204,5206,5208],{"class":369,"line":422},[16,5191,531],{"class":530},[16,5193,534],{"class":530},[16,5195,538],{"class":537},[16,5197,541],{"class":484},[16,5199,544],{"class":392},[16,5201,547],{"class":373},[16,5203,551],{"class":550},[16,5205,554],{"class":373},[16,5207,557],{"class":392},[16,5209,560],{"class":373},[16,5211,5212,5214,5216,5218,5220,5222,5224,5226],{"class":369,"line":460},[16,5213,565],{"class":392},[16,5215,2557],{"class":484},[16,5217,571],{"class":373},[16,5219,574],{"class":530},[16,5221,2564],{"class":537},[16,5223,541],{"class":377},[16,5225,551],{"class":484},[16,5227,584],{"class":377},[16,5229,5230],{"class":369,"line":481},[16,5231,589],{"emptyLinePlaceholder":268},[16,5233,5234,5236,5238,5240,5242,5244,5246,5248,5250,5252,5254],{"class":369,"line":488},[16,5235,565],{"class":392},[16,5237,3648],{"class":484},[16,5239,571],{"class":373},[16,5241,2557],{"class":484},[16,5243,25],{"class":373},[16,5245,2590],{"class":537},[16,5247,541],{"class":377},[16,5249,1012],{"class":373},[16,5251,4115],{"class":402},[16,5253,1012],{"class":373},[16,5255,584],{"class":377},[16,5257,5258,5260,5262,5264,5266,5268,5270,5272,5274,5276,5278,5280,5282,5284,5286,5288,5290,5292,5294,5296],{"class":369,"line":498},[16,5259,565],{"class":392},[16,5261,1222],{"class":484},[16,5263,571],{"class":373},[16,5265,574],{"class":530},[16,5267,4873],{"class":484},[16,5269,25],{"class":373},[16,5271,1130],{"class":537},[16,5273,541],{"class":377},[16,5275,1543],{"class":373},[16,5277,3648],{"class":484},[16,5279,677],{"class":373},[16,5281,554],{"class":377},[16,5283,25],{"class":373},[16,5285,1272],{"class":537},[16,5287,541],{"class":377},[16,5289,541],{"class":373},[16,5291,1029],{"class":550},[16,5293,554],{"class":373},[16,5295,557],{"class":392},[16,5297,560],{"class":373},[16,5299,5300,5302,5304,5306],{"class":369,"line":508},[16,5301,978],{"class":530},[16,5303,981],{"class":537},[16,5305,541],{"class":377},[16,5307,746],{"class":373},[16,5309,5310,5312,5314,5316],{"class":369,"line":792},[16,5311,990],{"class":377},[16,5313,754],{"class":373},[16,5315,995],{"class":617},[16,5317,767],{"class":373},[16,5319,5320,5322,5324,5326,5328,5330],{"class":369,"line":797},[16,5321,1002],{"class":377},[16,5323,754],{"class":373},[16,5325,683],{"class":373},[16,5327,1009],{"class":402},[16,5329,1012],{"class":373},[16,5331,767],{"class":373},[16,5333,5334,5336,5338,5340,5342,5344,5346,5348,5350,5352],{"class":369,"line":814},[16,5335,1020],{"class":377},[16,5337,754],{"class":373},[16,5339,1350],{"class":484},[16,5341,25],{"class":373},[16,5343,1034],{"class":484},[16,5345,1037],{"class":377},[16,5347,1040],{"class":617},[16,5349,1043],{"class":377},[16,5351,25],{"class":373},[16,5353,2954],{"class":484},[16,5355,5356,5358],{"class":369,"line":1017},[16,5357,1056],{"class":373},[16,5359,584],{"class":377},[16,5361,5362,5364],{"class":369,"line":1053},[16,5363,772],{"class":373},[16,5365,584],{"class":377},[16,5367,5368],{"class":369,"line":1061},[16,5369,589],{"emptyLinePlaceholder":268},[16,5371,5372],{"class":369,"line":1067},[16,5373,5374],{"class":594},"  \u002F\u002F Enregistrer le texte dans la base de données...\n",[16,5376,5377],{"class":369,"line":1072},[16,5378,589],{"emptyLinePlaceholder":268},[16,5380,5381,5383,5385,5387,5389,5391,5393,5395,5397,5399,5401,5403,5405],{"class":369,"line":1088},[16,5382,565],{"class":392},[16,5384,2581],{"class":484},[16,5386,571],{"class":373},[16,5388,2557],{"class":484},[16,5390,25],{"class":373},[16,5392,2590],{"class":537},[16,5394,541],{"class":377},[16,5396,1012],{"class":373},[16,5398,2491],{"class":402},[16,5400,1012],{"class":373},[16,5402,971],{"class":377},[16,5404,3916],{"class":530},[16,5406,3919],{"class":1299},[16,5408,5409,5411,5413,5415,5417],{"class":369,"line":1093},[16,5410,3924],{"class":537},[16,5412,541],{"class":377},[16,5414,2491],{"class":484},[16,5416,614],{"class":373},[16,5418,560],{"class":373},[16,5420,5421,5423,5425,5427,5430,5432],{"class":369,"line":1098},[16,5422,3937],{"class":377},[16,5424,754],{"class":373},[16,5426,683],{"class":373},[16,5428,5429],{"class":402},"1 Mo",[16,5431,1012],{"class":373},[16,5433,767],{"class":373},[16,5435,5436,5438,5440,5442,5444,5446,5448,5450,5452,5454,5456,5458,5460,5462,5464],{"class":369,"line":1103},[16,5437,3953],{"class":377},[16,5439,754],{"class":373},[16,5441,2795],{"class":377},[16,5443,1012],{"class":373},[16,5445,2800],{"class":402},[16,5447,1012],{"class":373},[16,5449,614],{"class":373},[16,5451,683],{"class":373},[16,5453,2809],{"class":402},[16,5455,1012],{"class":373},[16,5457,614],{"class":373},[16,5459,683],{"class":373},[16,5461,2818],{"class":402},[16,5463,1012],{"class":373},[16,5465,3982],{"class":377},[16,5467,5468,5470],{"class":369,"line":1120},[16,5469,772],{"class":373},[16,5471,584],{"class":377},[16,5473,5474],{"class":369,"line":1727},[16,5475,589],{"emptyLinePlaceholder":268},[16,5477,5478],{"class":369,"line":1744},[16,5479,2975],{"class":594},[16,5481,5482],{"class":369,"line":1755},[16,5483,589],{"emptyLinePlaceholder":268},[16,5485,5486,5488,5490,5492,5494,5496,5498],{"class":369,"line":1766},[16,5487,604],{"class":530},[16,5489,607],{"class":537},[16,5491,541],{"class":377},[16,5493,551],{"class":484},[16,5495,614],{"class":373},[16,5497,618],{"class":617},[16,5499,584],{"class":377},[16,5501,5502,5504],{"class":369,"line":1781},[16,5503,625],{"class":373},[16,5505,584],{"class":484},[13,5507,5508,5509,5511],{},"Pour des formulaires plus longs, je pourrais utiliser une boucle pour extraire chaque champ du formulaire et les valider. Dans cette boucle, je peux gérer des cas spécifiques comme des tableaux de champs multi-sélection ou des fichiers ou ignorer certains champs (qui pourraient être validés en utilisant ",[365,5510,2321],{},").",[359,5513,5515],{"className":521,"code":5514,"language":523,"meta":258,"style":258},"const data = {}\nfor (const [key, value] of formData.entries()) {\n  if (key === 'moduleId[]') {\n    data[key] = formData.getAll(key)\n    continue\n  }\n  else if (key === 'image') {\n    continue\n  }\n  else {\n    data[key] = value\n  }\n}\n",[365,5516,5517,5529,5565,5587,5614,5619,5623,5647,5651,5655,5661,5676,5680],{"__ignoreMap":258},[16,5518,5519,5521,5524,5526],{"class":369,"line":370},[16,5520,1510],{"class":392},[16,5522,5523],{"class":484}," data ",[16,5525,396],{"class":373},[16,5527,5528],{"class":373}," {}\n",[16,5530,5531,5534,5536,5538,5540,5543,5545,5548,5550,5553,5555,5557,5560,5563],{"class":369,"line":259},[16,5532,5533],{"class":530},"for",[16,5535,547],{"class":484},[16,5537,1510],{"class":392},[16,5539,2795],{"class":373},[16,5541,5542],{"class":484},"key",[16,5544,614],{"class":373},[16,5546,5547],{"class":484}," value",[16,5549,1043],{"class":373},[16,5551,5552],{"class":373}," of",[16,5554,2557],{"class":484},[16,5556,25],{"class":373},[16,5558,5559],{"class":537},"entries",[16,5561,5562],{"class":484},"()) ",[16,5564,746],{"class":373},[16,5566,5567,5569,5571,5573,5576,5578,5581,5583,5585],{"class":369,"line":422},[16,5568,955],{"class":530},[16,5570,547],{"class":377},[16,5572,5542],{"class":484},[16,5574,5575],{"class":373}," ===",[16,5577,683],{"class":373},[16,5579,5580],{"class":402},"moduleId[]",[16,5582,1012],{"class":373},[16,5584,971],{"class":377},[16,5586,746],{"class":373},[16,5588,5589,5592,5594,5596,5599,5601,5603,5605,5608,5610,5612],{"class":369,"line":460},[16,5590,5591],{"class":484},"    data",[16,5593,1037],{"class":377},[16,5595,5542],{"class":484},[16,5597,5598],{"class":377},"] ",[16,5600,396],{"class":373},[16,5602,2557],{"class":484},[16,5604,25],{"class":373},[16,5606,5607],{"class":537},"getAll",[16,5609,541],{"class":377},[16,5611,5542],{"class":484},[16,5613,584],{"class":377},[16,5615,5616],{"class":369,"line":481},[16,5617,5618],{"class":530},"    continue\n",[16,5620,5621],{"class":369,"line":488},[16,5622,1064],{"class":373},[16,5624,5625,5628,5631,5633,5635,5637,5639,5641,5643,5645],{"class":369,"line":498},[16,5626,5627],{"class":530},"  else",[16,5629,5630],{"class":530}," if",[16,5632,547],{"class":377},[16,5634,5542],{"class":484},[16,5636,5575],{"class":373},[16,5638,683],{"class":373},[16,5640,2491],{"class":402},[16,5642,1012],{"class":373},[16,5644,971],{"class":377},[16,5646,746],{"class":373},[16,5648,5649],{"class":369,"line":508},[16,5650,5618],{"class":530},[16,5652,5653],{"class":369,"line":792},[16,5654,1064],{"class":373},[16,5656,5657,5659],{"class":369,"line":797},[16,5658,5627],{"class":530},[16,5660,560],{"class":373},[16,5662,5663,5665,5667,5669,5671,5673],{"class":369,"line":814},[16,5664,5591],{"class":484},[16,5666,1037],{"class":377},[16,5668,5542],{"class":484},[16,5670,5598],{"class":377},[16,5672,396],{"class":373},[16,5674,5675],{"class":484}," value\n",[16,5677,5678],{"class":369,"line":1017},[16,5679,1064],{"class":373},[16,5681,5682],{"class":369,"line":1053},[16,5683,1816],{"class":373},[103,5685,5687],{"id":5686},"conclusion","Conclusion",[13,5689,5690,5691,5694,5695],{},"Et ",[67,5692,5693],{},"voilà"," ! Vous êtes maintenant capables de valider des formulaires à la fois côté client et côté serveur sans dupliquer la logique de validation. C'est un grand pas en avant dans la construction d'une application full-stack avec Nuxt. J'espère que cet article vous aidera à construire votre prochaine application en toute confiance. ",[67,5696,5697,5698,5703],{},"Vous devriez absolument consulter la ",[21,5699,5702],{"href":5700,"rel":5701},"https:\u002F\u002Fhub.nuxt.com\u002Fdocs",[33],"documentation de NuxtHub"," pour en savoir plus sur la plateforme et le pouvoir qu'elle offre pour construire des applications full-stack avec Nuxt.",[13,5705,5706],{},"N'hésitez pas à me faire savoir si vous avez des questions ou des commentaires. Je pourrais faire quelques erreurs, alors n'hésitez pas à me corriger. Je suis toujours à la recherche de moyens pour améliorer mon code et mes articles.",[13,5708,5709],{},"Merci de votre lecture !",[5711,5712,5713],"style",{},"html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}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 .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sHdIc, html code.shiki .sHdIc{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#EEFFFF;--shiki-default-font-style:italic;--shiki-dark:#BABED8;--shiki-dark-font-style:italic}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}",{"title":258,"searchDepth":259,"depth":259,"links":5715},[5716,5717],{"id":320,"depth":259,"text":321},{"id":353,"depth":259,"text":354,"children":5718},[5719],{"id":642,"depth":422,"text":643},"2024-06-07","Découvrez comment valider les formulaires à la fois côté client et côté serveur sans dupliquer la logique de validation.",{},"\u002Ffr\u002Fposts\u002Fnuxt-going-full-stack-how-to-streamline-form-validation","---\ncontentId: 8c077ba8-63ca-4784-b693-0e1fa8623bf9\ntitle: \"Nuxt Devient Full-Stack : Comment Valider des formulaires ?\"\ndescription: Découvrez comment valider les formulaires à la fois côté client et côté serveur sans dupliquer la logique de validation.\ndate: 2024-06-07\n---\n\nDepuis des semaines, je construis une application full-stack appelée Orion avec Nuxt et [NuxtHub](https:\u002F\u002Fhub.nuxt.com). Tout au long de ce parcours, j'ai rencontré de nombreux défis, et l'un d'eux est la validation des formulaires. Dans cet article, je vais vous montrer comment j'ai réussi à **valider les formulaires à la fois côté client et côté serveur sans duplications** de la logique de validation, des champs de texte simples aux téléchargements de fichiers complexes avec NuxtHub.\n\n_Orion est une collection de modèles basée sur la communauté pour votre prochain projet, des pages de destination aux applications web complètes. NuxtHub est une plateforme de déploiement et d'administration pour Nuxt, propulsée par Cloudflare._\n\n\u003CCalloutReadMore id=\"2349bbcd-1535-4f33-8da7-299f46af18b9\" \u002F>\n\n_Pendant cet article, je vais parler de [NuxtHub](https:\u002F\u002Fhub.nuxt.com) car il fournit les primitives pour construire des applications full-stack, mais même si vous ne l'utilisez pas, vous pouvez facilement suivre cet article. Les formulaires seront gérés sans aucune bibliothèque._\n\nVous pouvez parcourir le code exemple dans \u003CGitHubLink repo=\"barbapapazes\u002Fvalidate-forms-in-nuxt\" \u002F>.\n\n## Le Problème\n\nSur le web, les formulaires sont omniprésents. Ils sont le principal moyen d'interaction et de demande d'entrée utilisateur. Mais sur le web, il y a une règle essentielle : **ne jamais faire confiance au client** (absolument jamais et en aucun cas). Cela signifie que **toutes les données** provenant du client doivent être **vérifiées** et **sanitisées** sur le serveur avant d'être traitées ou stockées dans une base de données. Un client peut être un navigateur, une application mobile, une requête HTTP ou tout autre objet capable d'envoyer des données à votre serveur, construit par vous ou non.\n\nLorsque vous construisez une MPA traditionnelle (Multi-Page Application), seul le serveur est responsable de la validation. Pour une SPA (Single-Page Application), à la fois le client et le serveur sont responsables de la validation pour fournir une meilleure expérience utilisateur. _Cela est simplifié car il existe un moyen de donner une impression de validation côté client avec une MPA traditionnelle, mais ce n'est pas le sujet de cet article._ MAIS, même si les données sont validées côté client, il est **obligatoire** de les valider côté serveur.\n\n## Le Formulaire le Plus Simple\n\nCommençons notre parcours avec le formulaire le plus simple : une seule entrée de texte.\n\n```vue\n\u003Ctemplate>\n  \u003Cform method=\"post\" action=\"\u002Ftag\">\n    \u003Cinput type=\"text\" placeholder=\"Nom\" required>\n    \u003Cbutton type=\"submit\">\n      Créer Tag\n    \u003C\u002Fbutton>\n  \u003C\u002Fform>\n\u003C\u002Ftemplate>\n```\n\nEnsuite, côté serveur, je peux créer une route pour gérer la soumission du formulaire :\n\n```ts\nexport default defineEventHandler(async (event) => {\n  const body = await readBody(event)\n\n  \u002F\u002F Enregistrer dans la base de données...\n\n  return sendNoContent(event, 201)\n})\n```\n\nPour l'instant, je n'ai aucune validation. Je peux envoyer n'importe quoi au serveur, et il l'acceptera. _C'est un énorme problème de sécurité._\n\n_Rappelez-vous que la façon dont je gère la validation est spécifique à une SPA. Une MPA traditionnelle aurait ajouté des erreurs aux messages flash et redirigé la demande._\n\n### Valider le Corps\n\nPour valider le corps côté serveur, j'utilise [Zod](https:\u002F\u002Fzod.dev). _Zod est une validation de schéma en TypeScript avec inférence de type statique._ La bonne nouvelle est qu'h3, le routeur et le framework web qui alimentent Nitro, la couche serveur de Nuxt, fournit une utilitaire pour valider le corps : `readValidatedBody`.\n\n```ts\nimport { z } from 'zod'\n\nexport default defineEventHandler(async (event) => {\n  const body = await readValidatedBody(event, z.object({\n    name: z.string(),\n  }).parse)\n\n  \u002F\u002F Enregistrer dans la base de données...\n\n  return sendNoContent(event, 201)\n})\n```\n\nL'utilitaire `readValidatedBody` lancera automatiquement une erreur si le corps ne correspond pas au schéma. Par exemple, si le nom est manquant, la requête échouera.\n\n\u003CCallout icon=\"i-unjs-h3\" text=\"Valider les Données avec H3\" to=\"https:\u002F\u002Fh3.unjs.io\u002Fexamples\u002Fvalidate-data\" \u002F>\n\nPar défaut, l'erreur sera gérée par le serveur et lancera une erreur 500, mais je peux capturer l'erreur pour renvoyer un message d'erreur plus convivial.\n\nLa première solution consiste à utiliser la méthode `safeParse` qui ne lancera pas d'erreur mais renverra un résultat avec l'erreur le cas échéant.\n\n```ts\nimport { z } from 'zod'\n\nexport default defineEventHandler(async (event) => {\n  const result = await readValidatedBody(event, z.object({\n    name: z.string(),\n  }).safeParse)\n\n  if (!result.success) {\n    throw createError({\n      status: 400,\n      message: 'Données non valides',\n      data: result.error.errors[0].message,\n    })\n  }\n\n  const body = result.data\n\n  \u002F\u002F Enregistrer dans la base de données...\n\n  return sendNoContent(event, 201)\n})\n```\n\nLa seconde solution consiste à utiliser la méthode `parseAsync` pour attacher une gestion d'erreur manuelle à la promesse.\n\n```ts\nimport type { ZodError } from 'zod'\nimport { z } from 'zod'\n\nexport default defineEventHandler(async (event) => {\n  const body = await readValidatedBody(event, data => z.object({\n    name: z.string(),\n  }).parseAsync(data).catch((error) => {\n    if (error instanceof ZodError) {\n      throw createError({\n        status: 400,\n        message: 'Données non valides',\n        data: error.errors[0].message,\n      })\n    }\n  }))\n\n  \u002F\u002F Enregistrer dans la base de données...\n\n  return sendNoContent(event, 201)\n})\n```\n\n_Vous pourriez envelopper le validateur avec un `try\u002Fcatch`, mais je trouve cela moins élégant et lisible._\n\n### Valider le Frontend\n\nCôté frontend, je peux également utiliser Zod pour valider le formulaire avant de l'envoyer au serveur.\n\n```vue\n\u003Cscript lang=\"ts\" setup>\nimport type { ZodError } from 'zod'\nimport { z } from 'zod'\n\nconst schema = z.object({\n  name: z.string({ message: 'Obligatoire' }), \u002F\u002F Je peux personnaliser le message d'erreur affiché à l'utilisateur\n})\n\nconst state = reactive({\n  name: '',\n})\n\nasync function onSubmit() {\n  try {\n    const data = schema.parse(state)\n    await $fetch('\u002Ftag', {\n      method: 'post',\n      body: data,\n    })\n  }\n  catch (error) {\n    if (error instanceof ZodError) {\n      throw createError({\n        status: 400,\n        message: 'Données non valides',\n        data: error.errors,\n      })\n    }\n  }\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cform @submit.prevent=\"onSubmit()\">\n    \u003Cinput v-model=\"state.name\" type=\"text\" placeholder=\"Nom\" required>\n    \u003Cbutton type=\"submit\">\n      Créer Tag\n    \u003C\u002Fbutton>\n  \u003C\u002Fform>\n\u003C\u002Ftemplate>\n```\n\nL'implémentation est très simple et directe. Je pourrais enregistrer les erreurs dans une variable pour les afficher à l'utilisateur. Si vous souhaitez aller plus loin, je vous recommande d'essayer le composant `Form` de [Nuxt UI](https:\u002F\u002Fui.nuxt.com\u002F) ou une bibliothèque de validation comme [VeeValidate](https:\u002F\u002Fvee-validate.logaretm.com\u002Fv4\u002F) ou [FormKit](https:\u002F\u002Fformkit.com\u002F).\n\n## Refactorisons\n\nMaintenant que j'ai la logique de validation à la fois côté client et côté serveur, voyons s'il y a un code intéressant à refactoriser.\n\nLa première chose que je remarque est que la logique de validation, côté client et serveur, est la même, et c'est totalement normal. Mais que se passe-t-il si je décide de changer la logique de validation côté serveur parce que mon API est utilisée par une autre application ? Mon frontend pourrait être cassé car la logique de validation sera différente. Pour éviter cela, il serait peut-être mieux d'écrire le validateur une seule fois et de l'utiliser des deux côtés. Qu'en pensez-vous ?\n\nJe peux créer un fichier `validators.ts` dans le dossier `utils` de la partie client (à la racine) d'une application Nuxt. Ce fichier contiendra tous les validateurs pour mon application et sera partagé entre le client et le serveur.\n\n```ts\nimport { z } from 'zod'\n\nexport const createTagValidator = z.object({\n  name: z.string({ message: 'Obligatoire' }),\n})\n```\n\n> [!TIP]\n> Ce fichier peut être utilisé côté serveur mais a été créé côté client, donc vous devez explicitement importer tout ce dont vous avez besoin pour éviter des problèmes de rendu.\n\nCe validateur contient le message pour l'erreur pour aider l'utilisateur à comprendre ce qui ne va pas.\n\nGrâce à la fonction d'auto-import de Nuxt, je peux facilement l'utiliser côté client :\n\n```vue\n\u003Cscript lang=\"ts\" setup>\nconst schema = createTagValidator\n\n\u002F\u002F ...\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cform>\n  \u003C!-- ... -->\n  \u003C\u002Fform>\n\u003C\u002Ftemplate>\n```\n\nEnsuite, vous pouvez également l'utiliser côté serveur en l'importation :\n\n```ts\nimport { createTagValidator } from '~\u002Futils\u002Fvalidators'\n\nexport default defineEventHandler(async (event) => {\n  const body = await readValidatedBody(event, createTagValidator.parse)\n\n  \u002F\u002F Enregistrer dans la base de données...\n\n  return sendNoContent(event, 201)\n})\n```\n\nMaintenant, je peux changer en toute sécurité la logique de validation sans casser le frontend, car ils suivent les mêmes règles, et je n'ai pas à dupliquer le code.\n\n## Gestion des Fichiers\n\nPour cette partie, je vais vous montrer deux façons différentes de valider les fichiers côté serveur. La première utilisera Zod et quelques raffinements, et la seconde utilisera l'utilitaire `ensureBlob` de NuxtHub.\n\n_Cette validation n'est pas complète et devrait être améliorée pour gérer plus de cas._\n\n### Avec Zod\n\nPour cette seconde partie, je vais vous montrer comment télécharger un fichier sur le serveur. D'abord, ajoutons notre entrée dans le formulaire :\n\n```vue\n\u003Ctemplate>\n  \u003Cform action=\"\u002Fapi\u002Fimage\" enctype=\"multipart\u002Fform-data\" method=\"post\">\n    \u003Cinput type=\"file\" accept=\"image\u002Fpng, image\u002Fjpeg, image\u002Fjpg\" required>\n    \u003Cbutton type=\"submit\">\n      Créer Tag\n    \u003C\u002Fbutton>\n  \u003C\u002Fform>\n\u003C\u002Ftemplate>\n```\n\nEnsuite, je peux gérer le fichier côté serveur, mais c'est un peu plus complexe car j'envoie un `formData` et h3 ne fournit pas d'utilitaire comme `readValidatedFormData`. Donc, je dois le gérer manuellement.\n\nD'abord, je vais lire le `formData` en utilisant l'utilitaire `readFormData`, puis j'extraire l'`image` du `formData` et la valider en utilisant Zod.\n\n```ts\nimport { any, object } from 'zod'\n\nexport default defineEventHandler(async (event) => {\n  const formData = await readFormData(event)\n\n  const image = formData.get('image')\n  await object({\n    image: any()\n      .refine(image => image instanceof File, { message: 'L\\'image doit être un fichier' })\n      .refine(image => image.size \u003C 1024 * 1024, { message: 'L\\'image doit faire moins de 1 Mo' })\n      .refine(image => image.type.startsWith('image\u002F'), { message: 'L\\'image doit être une image' })\n      .refine(image => ['image\u002Fpng', 'image\u002Fjpeg', 'image\u002Fjpg'].includes(image.type), { message: 'L\\'image doit être un JPEG ou un PNG' })\n  }).parseAsync({ image }).catch((error) => {\n    throw createError({\n      status: 400,\n      message: 'Image non valide',\n      data: error.errors[0].message\n    })\n  })\n\n  \u002F\u002F Enregistrer l'image dans une base de données ou un système de fichiers\n\n  return sendNoContent(event, 201)\n})\n```\n\nCôté client, je peux également valider le fichier en utilisant la même technique qu'auparavant et en partageant le validateur entre le client et le serveur.\n\nD'abord, je dois déplacer le validateur dans le fichier `utils\u002Fvalidators.ts` :\n\n```ts\nimport { any, object } from 'zod'\n\nexport const createImageValidator = object({\n  image: any()\n    .refine(image => image instanceof File, { message: 'L\\'image doit être un fichier' })\n    .refine(image => image.size \u003C 1024 * 1024, { message: 'L\\'image doit faire moins de 1 Mo' })\n    .refine(image => image.type.startsWith('image\u002F'), { message: 'L\\'image doit être une image' })\n    .refine(image => ['image\u002Fjpeg', 'image\u002Fpng'].includes(image.type), { message: 'L\\'image doit être un JPEG ou un PNG' })\n})\n```\n\nEnsuite, je peux l'utiliser lors de la soumission du formulaire pour éviter d'envoyer des données non valides au serveur :\n\n```vue\n\u003Cscript lang=\"ts\" setup>\nasync function onImageSubmit(event: Event) {\n  const target = event.target as HTMLFormElement\n  const formData = new FormData(target)\n\n  const image = formData.get('image')\n  try {\n    await createImageValidator.parseAsync({ image })\n\n    await $fetch('\u002Fapi\u002Fimage', {\n      method: 'POST',\n      body: formData,\n    })\n  }\n  catch (error) {\n    console.error(error)\n  }\n}\n\u003C\u002Fscript>\n\n\u003Ctemplate>\n  \u003Cdiv>\n    \u003Ch1>Valider les Formulaires avec Nuxt\u003C\u002Fh1>\n\n    \u003Ch2>\n      Image\n    \u003C\u002Fh2>\n    \u003Cform enctype=\"multipart\u002Fform-data\" @submit.prevent=\"onImageSubmit($event)\">\n      \u003Cinput type=\"file\" name=\"image\" accept=\"image\u002Fpng, image\u002Fjpeg, image\u002Fjpg\">\n      \u003Cbutton type=\"submit\">\n        Soumettre\n      \u003C\u002Fbutton>\n    \u003C\u002Fform>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n```\n\nSi la validation échoue, la requête ne sera pas envoyée au serveur et l'erreur sera affichée dans la console. Bien sûr, vous pouvez gérer l'erreur comme vous le souhaitez, comme utiliser un `toast` ou afficher un message d'erreur sous l'entrée concernée.\n\nCôté serveur, je peux remplacer la logique de validation par le validateur :\n\n```ts\nimport { saveImageValidator } from '~\u002Futils\u002Fvalidators'\n\nexport default defineEventHandler(async (event) => {\n  \u002F\u002F ...\n\n  await saveImageValidator.parseAsync({ image })\n\n  \u002F\u002F ...\n})\n```\n\n### Avec NuxtHub\n\nNuxtHub est livré avec un utilitaire nommé `ensureBlob` qui peut être utilisé pour vérifier la taille et le type du fichier. Cet utilitaire est très utile car il lancera automatiquement une erreur si le fichier n'est pas valide, mais ne peut être utilisé que côté serveur. Cela signifie que j'aurai un schéma Zod côté client et un utilitaire NuxtHub côté serveur. _Utilisez des constantes pour partager les limites de validation entre le schéma Zod et l'utilitaire NuxtHub._\n\n```ts\nexport default defineEventHandler(async (event) => {\n  const formData = await readFormData(event)\n\n  const image = formData.get('image') as Blob\n  ensureBlob(image, {\n    maxSize: '1Mo',\n    types: ['image\u002Fpng', 'image\u002Fjpeg', 'image\u002Fjpg']\n  })\n\n  \u002F\u002F Enregistrer l'image dans une base de données ou un système de fichiers\n\n  return sendNoContent(event, 201)\n})\n```\n\n## Texte et Blob dans le Même Formulaire\n\nJusqu'à présent, j'ai seulement un type d'entrée dans mon formulaire, texte ou fichier. Mais que se passe-t-il si je veux avoir les deux dans le même formulaire ?\n\nVous devez savoir que vous devez absolument utiliser un `formData` pour envoyer le formulaire au serveur en raison de l'entrée de fichier.\n\nLe formulaire est toujours très simple :\n\n```vue\n\u003Ctemplate>\n  \u003Cform action=\"\u002Fapi\u002Ftext-image\" enctype=\"multipart\u002Fform-data\" method=\"post\">\n    \u003Cinput type=\"text\" name=\"name\" placeholder=\"Nom\" required>\n    \u003Cinput type=\"file\" name=\"image\" accept=\"image\u002Fpng, image\u002Fjpeg, image\u002Fjpg\" required>\n    \u003Cbutton type=\"submit\">\n      Créer Tag\n    \u003C\u002Fbutton>\n  \u003C\u002Fform>\n\u003C\u002Ftemplate>\n```\n\nMaintenant, je peux créer un validateur en utilisant Zod :\n\n```ts\nimport { any, object, string } from 'zod'\n\nexport const createValidator = object({\n  name: string({ message: 'Obligatoire' }),\n  image: any().refine(image => image instanceof File, { message: 'L\\'image doit être un fichier' }).refine(image => image.size \u003C 1024 * 1024, { message: 'L\\'image doit faire moins de 1 Mo' }).refine(image => image.type.startsWith('image\u002F'), { message: 'L\\'image doit être une image' }).refine(image => ['image\u002Fpng', 'image\u002Fjpeg', 'image\u002Fjpg'].includes(image.type), { message: 'L\\'image doit être un JPEG ou un PNG' })\n})\n```\n\nCe validateur peut être utilisé comme auparavant, sur le serveur et le client.\n\nSi vous préférez utiliser l'utilitaire `ensureBlob` de NuxtHub, je vous recommande de diviser le validateur en deux parties : une pour le texte et une pour l'image. Cela signifie que le texte et l'image seront utilisés côté client en les fusionnant en un seul objet, et uniquement le texte sera utilisé côté serveur.\n\n```ts\nimport { any, object, string } from 'zod'\n\nexport const createTextValidator = object({\n  name: string({ message: 'Obligatoire' }),\n})\n\nexport const createImageValidator = object({\n  image: any().refine(image => image instanceof File, { message: 'L\\'image doit être un fichier' }).refine(image => image.size \u003C 1024 * 1024, { message: 'L\\'image doit faire moins de 1 Mo' }).refine(image => image.type.startsWith('image\u002F'), { message: 'L\\'image doit être une image' }).refine(image => ['image\u002Fpng', 'image\u002Fjpeg', 'image\u002Fjpg'].includes(image.type), { message: 'L\\'image doit être un JPEG ou un PNG' })\n})\n\nexport const createValidator = createTextValidator.merge(createImageValidator)\n```\n\nCôté client, j'utiliserai le `createValidator` pour valider le formulaire avant de l'envoyer au serveur :\n\n```vue\n\u003Cscript lang=\"ts\" setup>\nasync function onSubmit(event: Event) {\n  const target = event.target as HTMLFormElement\n  const formData = new FormData(target)\n\n  const name = formData.get('name')\n  const image = formData.get('image')\n  try {\n    await createValidator.parseAsync({ name, image })\n\n    await $fetch('\u002Fapi\u002Ftext-image', {\n      method: 'POST',\n      body: formData,\n    })\n  }\n  catch (error) {\n    console.error(error)\n  }\n}\n\u003C\u002Fscript>\n```\n\nCôté serveur, j'utiliserai le `createTextValidator` pour valider le texte et l'utilitaire `ensureBlob` pour valider l'image :\n\n```ts\nimport { createTextValidator } from '~\u002Futils\u002Fvalidators'\n\nexport default defineEventHandler(async (event) => {\n  const formData = await readFormData(event)\n\n  const name = formData.get('name')\n  const data = await createTextValidator.parseAsync({ name }).catch((error) => {\n    throw createError({\n      status: 400,\n      message: 'Données non valides',\n      data: error.errors[0].message\n    })\n  })\n\n  \u002F\u002F Enregistrer le texte dans la base de données...\n\n  const image = formData.get('image') as Blob\n  ensureBlob(image, {\n    maxSize: '1 Mo',\n    types: ['image\u002Fpng', 'image\u002Fjpeg', 'image\u002Fjpg']\n  })\n\n  \u002F\u002F Enregistrer l'image dans une base de données ou un système de fichiers\n\n  return sendNoContent(event, 201)\n})\n```\n\nPour des formulaires plus longs, je pourrais utiliser une boucle pour extraire chaque champ du formulaire et les valider. Dans cette boucle, je peux gérer des cas spécifiques comme des tableaux de champs multi-sélection ou des fichiers ou ignorer certains champs (qui pourraient être validés en utilisant `ensureBlob`).\n\n```ts\nconst data = {}\nfor (const [key, value] of formData.entries()) {\n  if (key === 'moduleId[]') {\n    data[key] = formData.getAll(key)\n    continue\n  }\n  else if (key === 'image') {\n    continue\n  }\n  else {\n    data[key] = value\n  }\n}\n```\n\n## Conclusion\n\nEt _voilà_ ! Vous êtes maintenant capables de valider des formulaires à la fois côté client et côté serveur sans dupliquer la logique de validation. C'est un grand pas en avant dans la construction d'une application full-stack avec Nuxt. J'espère que cet article vous aidera à construire votre prochaine application en toute confiance. _Vous devriez absolument consulter la [documentation de NuxtHub](https:\u002F\u002Fhub.nuxt.com\u002Fdocs) pour en savoir plus sur la plateforme et le pouvoir qu'elle offre pour construire des applications full-stack avec Nuxt._\n\nN'hésitez pas à me faire savoir si vous avez des questions ou des commentaires. Je pourrais faire quelques erreurs, alors n'hésitez pas à me corriger. Je suis toujours à la recherche de moyens pour améliorer mon code et mes articles.\n\nMerci de votre lecture !\n",{"title":281,"description":5721},{"loc":5723},"fr\u002Fposts\u002Fnuxt-going-full-stack-how-to-streamline-form-validation","UnvxnnkvgTeO4duH9JEcZsC_VwGrInWRErZmVACOu-U",1790086904215]