[{"data":1,"prerenderedAt":371},["ShallowReactive",2],{"article-\u002Fblog\u002Fcloudflare-r2-stockage-objets-sans-frais-de-sortie":3},{"id":4,"title":5,"body":6,"brouillon":357,"date":358,"description":359,"extension":360,"image":361,"imageAlt":361,"meta":362,"navigation":86,"path":363,"seo":364,"stem":365,"tags":366,"__hash__":370},"blog\u002Fblog\u002Fcloudflare-r2-stockage-objets-sans-frais-de-sortie.md","Cloudflare R2 : du stockage objet sans frais de sortie pour vos applications",{"type":7,"value":8,"toc":350},"minimark",[9,18,23,30,34,41,45,48,180,187,306,309,313,339,343,346],[10,11,12,13,17],"p",{},"Quand une application doit stocker des fichiers utilisateurs (documents,\nimages, PDF), le réflexe est souvent Amazon S3. Mais pour un projet dont le\nbudget se compte en euros par mois plutôt qu'en milliers, la facture S3\ncache un piège : les ",[14,15,16],"strong",{},"frais de sortie"," (egress). Chaque téléchargement de\nfichier par vos utilisateurs vous coûte de la bande passante facturée.\nC'est précisément ce que Cloudflare R2 élimine.",[19,20,22],"h2",{"id":21},"ce-quest-r2-en-une-phrase","Ce qu'est R2, en une phrase",[10,24,25,26,29],{},"R2 est un stockage objet compatible avec l'API S3 (mêmes SDK, mêmes\nconcepts de buckets et de clés), avec une différence économique majeure :\n",[14,27,28],{},"zéro frais de sortie",". Vous payez le stockage (environ 0,015 $\u002FGo\u002Fmois)\net les opérations, pas les téléchargements.",[19,31,33],{"id":32},"pourquoi-cest-décisif-pour-une-app-à-documents","Pourquoi c'est décisif pour une app à documents",[10,35,36,37,40],{},"Prenons une application de dossiers médicaux où les utilisateurs consultent\nrégulièrement leurs documents. Le ratio lecture\u002Fécriture est très\ndéséquilibré : un document uploadé une fois sera téléchargé des dizaines de\nfois. Sur S3, ce trafic de lecture est le poste de coût principal et,\nsurtout, il est ",[14,38,39],{},"imprévisible"," : il croît avec l'usage, pas avec le\nvolume stocké. Sur R2, le coût est fonction du seul volume : prévisible,\nbudgétable, sans mauvaise surprise en cas de succès.",[19,42,44],{"id":43},"lintégration-côté-nestjs","L'intégration côté NestJS",[10,46,47],{},"R2 étant compatible S3, le SDK AWS officiel fonctionne tel quel, seul\nl'endpoint change :",[49,50,55],"pre",{"className":51,"code":52,"language":53,"meta":54,"style":54},"language-typescript shiki shiki-themes github-dark","import { S3Client } from '@aws-sdk\u002Fclient-s3';\n\nconst r2 = new S3Client({\n  region: 'auto',\n  endpoint: `https:\u002F\u002F${accountId}.r2.cloudflarestorage.com`,\n  credentials: {\n    accessKeyId: process.env.R2_ACCESS_KEY_ID,\n    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,\n  },\n});\n","typescript","",[56,57,58,81,88,111,123,140,146,157,168,174],"code",{"__ignoreMap":54},[59,60,63,67,71,74,78],"span",{"class":61,"line":62},"line",1,[59,64,66],{"class":65},"snl16","import",[59,68,70],{"class":69},"s95oV"," { S3Client } ",[59,72,73],{"class":65},"from",[59,75,77],{"class":76},"sU2Wk"," '@aws-sdk\u002Fclient-s3'",[59,79,80],{"class":69},";\n",[59,82,84],{"class":61,"line":83},2,[59,85,87],{"emptyLinePlaceholder":86},true,"\n",[59,89,91,94,98,101,104,108],{"class":61,"line":90},3,[59,92,93],{"class":65},"const",[59,95,97],{"class":96},"sDLfK"," r2",[59,99,100],{"class":65}," =",[59,102,103],{"class":65}," new",[59,105,107],{"class":106},"svObZ"," S3Client",[59,109,110],{"class":69},"({\n",[59,112,114,117,120],{"class":61,"line":113},4,[59,115,116],{"class":69},"  region: ",[59,118,119],{"class":76},"'auto'",[59,121,122],{"class":69},",\n",[59,124,126,129,132,135,138],{"class":61,"line":125},5,[59,127,128],{"class":69},"  endpoint: ",[59,130,131],{"class":76},"`https:\u002F\u002F${",[59,133,134],{"class":69},"accountId",[59,136,137],{"class":76},"}.r2.cloudflarestorage.com`",[59,139,122],{"class":69},[59,141,143],{"class":61,"line":142},6,[59,144,145],{"class":69},"  credentials: {\n",[59,147,149,152,155],{"class":61,"line":148},7,[59,150,151],{"class":69},"    accessKeyId: process.env.",[59,153,154],{"class":96},"R2_ACCESS_KEY_ID",[59,156,122],{"class":69},[59,158,160,163,166],{"class":61,"line":159},8,[59,161,162],{"class":69},"    secretAccessKey: process.env.",[59,164,165],{"class":96},"R2_SECRET_ACCESS_KEY",[59,167,122],{"class":69},[59,169,171],{"class":61,"line":170},9,[59,172,173],{"class":69},"  },\n",[59,175,177],{"class":61,"line":176},10,[59,178,179],{"class":69},"});\n",[10,181,182,183,186],{},"Le pattern que je recommande pour les fichiers privés : les ",[14,184,185],{},"URLs\nprésignées",". Le backend ne sert jamais les octets lui-même : il génère une\nURL temporaire (quelques minutes de validité) que le client utilise pour\nuploader ou télécharger directement auprès de R2 :",[49,188,190],{"className":51,"code":189,"language":53,"meta":54,"style":54},"import { GetObjectCommand } from '@aws-sdk\u002Fclient-s3';\nimport { getSignedUrl } from '@aws-sdk\u002Fs3-request-presigner';\n\nasync getDownloadUrl(key: string): Promise\u003Cstring> {\n  const command = new GetObjectCommand({ Bucket: 'documents', Key: key });\n  return getSignedUrl(this.r2, command, { expiresIn: 300 });\n}\n",[56,191,192,205,219,223,249,271,301],{"__ignoreMap":54},[59,193,194,196,199,201,203],{"class":61,"line":62},[59,195,66],{"class":65},[59,197,198],{"class":69}," { GetObjectCommand } ",[59,200,73],{"class":65},[59,202,77],{"class":76},[59,204,80],{"class":69},[59,206,207,209,212,214,217],{"class":61,"line":83},[59,208,66],{"class":65},[59,210,211],{"class":69}," { getSignedUrl } ",[59,213,73],{"class":65},[59,215,216],{"class":76}," '@aws-sdk\u002Fs3-request-presigner'",[59,218,80],{"class":69},[59,220,221],{"class":61,"line":90},[59,222,87],{"emptyLinePlaceholder":86},[59,224,225,228,231,234,237,240,243,246],{"class":61,"line":113},[59,226,227],{"class":69},"async ",[59,229,230],{"class":106},"getDownloadUrl",[59,232,233],{"class":69},"(key: string): ",[59,235,236],{"class":96},"Promise",[59,238,239],{"class":65},"\u003C",[59,241,242],{"class":69},"string",[59,244,245],{"class":65},">",[59,247,248],{"class":69}," {\n",[59,250,251,254,257,259,262,265,268],{"class":61,"line":125},[59,252,253],{"class":69},"  const command ",[59,255,256],{"class":65},"=",[59,258,103],{"class":65},[59,260,261],{"class":106}," GetObjectCommand",[59,263,264],{"class":69},"({ Bucket: ",[59,266,267],{"class":76},"'documents'",[59,269,270],{"class":69},", Key: key });\n",[59,272,273,276,279,282,286,289,292,295,298],{"class":61,"line":142},[59,274,275],{"class":69},"  return ",[59,277,278],{"class":106},"getSignedUrl",[59,280,281],{"class":69},"(this.r2, ",[59,283,285],{"class":284},"s9osk","command",[59,287,288],{"class":69},", { ",[59,290,291],{"class":284},"expiresIn",[59,293,294],{"class":69},": ",[59,296,297],{"class":96},"300",[59,299,300],{"class":69}," });\n",[59,302,303],{"class":61,"line":148},[59,304,305],{"class":69},"}\n",[10,307,308],{},"Bénéfices : votre serveur applicatif (un petit VPS, par exemple) n'est\njamais le goulot d'étranglement des transferts de fichiers, et le contrôle\nd'accès reste entièrement chez vous : c'est votre API qui décide qui obtient\nune URL, et pour combien de temps.",[19,310,312],{"id":311},"les-pièges-à-connaître","Les pièges à connaître",[314,315,316,323,333],"ul",{},[317,318,319,322],"li",{},[14,320,321],{},"La compatibilité S3 n'est pas totale."," L'essentiel y est (put, get,\nmultipart, presign), mais certaines fonctionnalités avancées de S3\nmanquent. Vérifiez la liste officielle si vous migrez un code existant.",[317,324,325,328,329,332],{},[14,326,327],{},"Nommez vos clés avec discipline"," dès le premier jour\n(",[56,330,331],{},"users\u002F{id}\u002Fdocuments\u002F{uuid}.pdf",") : le stockage objet n'a pas de\ndossiers, et une convention de nommage propre est votre seul outil\nd'organisation et de purge.",[317,334,335,338],{},[14,336,337],{},"Ne mettez jamais de données sensibles dans les noms de clés"," : ils\napparaissent dans les URLs et les logs.",[19,340,342],{"id":341},"la-leçon","La leçon",[10,344,345],{},"Pour une application qui sert beaucoup de fichiers avec un budget contraint,\nle critère n'est pas « quel est le stockage le moins cher au Go » mais\n« quel est le coût total quand mes utilisateurs utilisent vraiment\nl'application ». R2 rend ce coût prévisible, et pour un produit qui doit\nvivre des années, la prévisibilité est une fonctionnalité.",[347,348,349],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}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 pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":54,"searchDepth":83,"depth":83,"links":351},[352,353,354,355,356],{"id":21,"depth":83,"text":22},{"id":32,"depth":83,"text":33},{"id":43,"depth":83,"text":44},{"id":311,"depth":83,"text":312},{"id":341,"depth":83,"text":342},false,"2026-05-12","Pourquoi R2 plutôt que S3 pour stocker les fichiers d'une application : architecture, URLs présignées avec NestJS et pièges à éviter.","md",null,{},"\u002Fblog\u002Fcloudflare-r2-stockage-objets-sans-frais-de-sortie",{"title":5,"description":359},"blog\u002Fcloudflare-r2-stockage-objets-sans-frais-de-sortie",[367,368,369],"Cloud","NestJS","Architecture","x4LMwsQCX7dcwVdTSyZl61kUaQH9SCb4mhcUHa3nWh4",1784652016702]