[{"data":1,"prerenderedAt":1400},["ShallowReactive",2],{"article-fr-\u002Fbackend\u002Fapi-design\u002Fprotobuf-fieldmask":3,"article-sibling-fr-\u002Fbackend\u002Fapi-design\u002Fprotobuf-fieldmask":747,"surround-fr-\u002Fbackend\u002Fapi-design\u002Fprotobuf-fieldmask":1363,"related-fr-\u002Fbackend\u002Fapi-design\u002Fprotobuf-fieldmask":1370},{"id":4,"title":5,"body":6,"date":729,"description":730,"draft":731,"extension":732,"img":733,"meta":734,"navigation":83,"path":735,"seo":736,"slug":737,"stem":738,"tags":739,"topics":744,"__hash__":746},"content\u002F2.backend\u002F2.api-design\u002F1.protobuf-fieldmask.fr.md","Protobuf FieldMask : laissez vos clients d'API ne demander que ce dont ils ont besoin",{"type":7,"value":8,"toc":720},"minimark",[9,13,22,30,41,46,49,132,135,206,222,226,229,263,274,277,281,295,314,335,338,360,363,371,375,382,388,398,512,523,533,537,544,568,591,595,660,664,713,716],[10,11,5],"h1",{"id":12},"protobuf-fieldmask-laissez-vos-clients-dapi-ne-demander-que-ce-dont-ils-ont-besoin",[14,15,16,17,21],"p",{},"Un endpoint est rarement utilisé par un seul client. Une app mobile, un front web, un job batch et un autre service backend tapent tous le même ",[18,19,20],"code",{},"GetProduct"," et ils ne veulent pas la même chose. L'app mobile veut un titre et un prix. La page produit veut aussi les avis et les recommandations. Le batch ne veut qu'un ID et une date.",[14,23,24,25,29],{},"Le problème : si le serveur calcule ",[26,27,28],"em",{},"tout"," à chaque fois, l'appelant le moins gourmand paie le prix du plus gourmand. Certains champs sont gratuits (ils sont déjà dans la ligne qu'on a lue). D'autres non : ils veulent dire une agrégation SQL, un appel à un autre service, ou un gros payload sur une connexion mobile lente.",[14,31,32,36,37,40],{},[33,34,35],"strong",{},"FieldMask"," est une manière standard et minimale pour le client de dire ",[26,38,39],{},"« ne me renvoie que ces champs »",", et pour le serveur de sauter le travail derrière les champs que personne n'a demandés. Cet article explique l'idée avec une API de boutique en ligne classique, pas une recette à copier-coller, pour que vous puissiez l'appliquer à vos propres services.",[42,43,45],"h2",{"id":44},"un-scénario-classique","Un scénario classique",[14,47,48],{},"Imaginons une boutique en ligne. Votre service produit expose une méthode gRPC :",[50,51,56],"pre",{"className":52,"code":53,"language":54,"meta":55,"style":55},"language-protobuf shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","service ProductService {\n  rpc GetProduct(GetProductRequest) returns (Product);\n}\n\nmessage Product {\n  string id             = 1;\n  string title          = 2;\n  Money  price          = 3;\n  ReviewSummary reviews = 4;  \u002F\u002F agrégation sur la table des avis\n  repeated Product recommendations = 5;  \u002F\u002F appel au service de reco\n  StockInfo stock       = 6;  \u002F\u002F fan-out vers chaque entrepôt\n}\n","protobuf","",[18,57,58,66,72,78,85,91,97,103,109,115,121,127],{"__ignoreMap":55},[59,60,63],"span",{"class":61,"line":62},"line",1,[59,64,65],{},"service ProductService {\n",[59,67,69],{"class":61,"line":68},2,[59,70,71],{},"  rpc GetProduct(GetProductRequest) returns (Product);\n",[59,73,75],{"class":61,"line":74},3,[59,76,77],{},"}\n",[59,79,81],{"class":61,"line":80},4,[59,82,84],{"emptyLinePlaceholder":83},true,"\n",[59,86,88],{"class":61,"line":87},5,[59,89,90],{},"message Product {\n",[59,92,94],{"class":61,"line":93},6,[59,95,96],{},"  string id             = 1;\n",[59,98,100],{"class":61,"line":99},7,[59,101,102],{},"  string title          = 2;\n",[59,104,106],{"class":61,"line":105},8,[59,107,108],{},"  Money  price          = 3;\n",[59,110,112],{"class":61,"line":111},9,[59,113,114],{},"  ReviewSummary reviews = 4;  \u002F\u002F agrégation sur la table des avis\n",[59,116,118],{"class":61,"line":117},10,[59,119,120],{},"  repeated Product recommendations = 5;  \u002F\u002F appel au service de reco\n",[59,122,124],{"class":61,"line":123},11,[59,125,126],{},"  StockInfo stock       = 6;  \u002F\u002F fan-out vers chaque entrepôt\n",[59,128,130],{"class":61,"line":129},12,[59,131,77],{},[14,133,134],{},"Regardez le coût de chaque champ :",[136,137,138,151],"table",{},[139,140,141],"thead",{},[142,143,144,148],"tr",{},[145,146,147],"th",{},"Champ",[145,149,150],{},"Coût pour le produire",[152,153,154,172,186,196],"tbody",{},[142,155,156,169],{},[157,158,159,162,163,162,166],"td",{},[18,160,161],{},"id",", ",[18,164,165],{},"title",[18,167,168],{},"price",[157,170,171],{},"gratuit, déjà dans la ligne produit",[142,173,174,179],{},[157,175,176],{},[18,177,178],{},"reviews",[157,180,181,182,185],{},"un ",[18,183,184],{},"GROUP BY"," sur des millions de lignes",[142,187,188,193],{},[157,189,190],{},[18,191,192],{},"recommendations",[157,194,195],{},"un appel distant au service de recommandation",[142,197,198,203],{},[157,199,200],{},[18,201,202],{},"stock",[157,204,205],{},"un fan-out vers chaque entrepôt, puis une somme",[14,207,208,209,211,212,214,215,217,218,221],{},"Un téléphone qui affiche un résultat de recherche n'a besoin que de ",[18,210,165],{}," et ",[18,213,168],{},". Mais si ",[18,216,20],{}," construit toujours le ",[18,219,220],{},"Product"," complet, ce téléphone déclenche une agrégation d'avis, un appel de reco et un fan-out de stock qu'il n'affichera jamais. Multipliez par des milliers de requêtes par seconde et vous brûlez du CPU, vous saturez les services en aval, et vous envoyez des octets que personne ne lit.",[42,223,225],{"id":224},"la-solution-tentante-qui-ne-passe-pas-à-léchelle","La solution tentante qui ne passe pas à l'échelle",[14,227,228],{},"Le premier réflexe, ce sont les booléens :",[50,230,232],{"className":52,"code":231,"language":54,"meta":55,"style":55},"message GetProductRequest {\n  string id = 1;\n  bool include_reviews = 2;\n  bool include_recommendations = 3;\n  bool include_stock = 4;\n}\n",[18,233,234,239,244,249,254,259],{"__ignoreMap":55},[59,235,236],{"class":61,"line":62},[59,237,238],{},"message GetProductRequest {\n",[59,240,241],{"class":61,"line":68},[59,242,243],{},"  string id = 1;\n",[59,245,246],{"class":61,"line":74},[59,247,248],{},"  bool include_reviews = 2;\n",[59,250,251],{"class":61,"line":80},[59,252,253],{},"  bool include_recommendations = 3;\n",[59,255,256],{"class":61,"line":87},[59,257,258],{},"  bool include_stock = 4;\n",[59,260,261],{"class":61,"line":93},[59,262,77],{},[14,264,265,266,269,270,273],{},"Ça marche, jusqu'à ce que ça ne marche plus. Chaque nouveau champ coûteux ajoute un flag ",[18,267,268],{},"include_*",". Les champs imbriqués (je veux ",[18,271,272],{},"reviews.average"," mais pas le détail complet) n'ont aucune expression propre. Et chaque client doit connaître votre liste privée de flags. C'est un casse-tête combinatoire qui grossit avec votre schéma.",[14,275,276],{},"Ce qu'on veut, c'est un seul mécanisme qui passe à l'échelle sur n'importe quel champ, s'imbrique naturellement, et fait partie de la boîte à outils standard. C'est FieldMask.",[42,278,280],{"id":279},"ce-quest-réellement-fieldmask","Ce qu'est réellement FieldMask",[14,282,283,286,287,290,291,294],{},[18,284,285],{},"google.protobuf.FieldMask"," est un ",[33,288,289],{},"type bien connu"," (",[26,292,293],{},"well-known type",") livré avec Protocol Buffers. C'est presque rien : juste une liste de chemins de champs :",[50,296,298],{"className":52,"code":297,"language":54,"meta":55,"style":55},"message FieldMask {\n  repeated string paths = 1;\n}\n",[18,299,300,305,310],{"__ignoreMap":55},[59,301,302],{"class":61,"line":62},[59,303,304],{},"message FieldMask {\n",[59,306,307],{"class":61,"line":68},[59,308,309],{},"  repeated string paths = 1;\n",[59,311,312],{"class":61,"line":74},[59,313,77],{},[14,315,316,317,162,319,162,321,323,324,327,328,290,331,334],{},"Un chemin est un nom de champ, séparé par des points pour l'imbrication : ",[18,318,165],{},[18,320,168],{},[18,322,272],{},". C'est tout. Le client le remplit, le serveur le lit. C'est le cousin gRPC du ",[18,325,326],{},"?fields=title,price"," de REST et de la sélection de champs de GraphQL, un ",[33,329,330],{},"jeu de champs partiel",[26,332,333],{},"sparse fieldset",") : demandez un sous-ensemble, recevez un sous-ensemble.",[14,336,337],{},"Vous ajoutez un champ à votre requête :",[50,339,341],{"className":52,"code":340,"language":54,"meta":55,"style":55},"message GetProductRequest {\n  string id = 1;\n  google.protobuf.FieldMask field_mask = 2;\n}\n",[18,342,343,347,351,356],{"__ignoreMap":55},[59,344,345],{"class":61,"line":62},[59,346,238],{},[59,348,349],{"class":61,"line":68},[59,350,243],{},[59,352,353],{"class":61,"line":74},[59,354,355],{},"  google.protobuf.FieldMask field_mask = 2;\n",[59,357,358],{"class":61,"line":80},[59,359,77],{},[14,361,362],{},"Et le client demande exactement ce dont il a besoin :",[50,364,369],{"className":365,"code":367,"language":368},[366],"language-text","field_mask {\n  paths: \"title\"\n  paths: \"price\"\n}\n","text",[18,370,367],{"__ignoreMap":55},[42,372,374],{"id":373},"lire-avec-un-masque","Lire avec un masque",[14,376,377,378,381],{},"Voici le flux. Le masque fait ",[33,379,380],{},"deux tâches distinctes",", et il ne faut pas les confondre :",[50,383,386],{"className":384,"code":385,"language":368},[366],"Client  ── field_mask { title, price } ──►  Serveur\n                                             │\n                                             │ 1. GARDE : ne fait le travail\n                                             │    coûteux que pour les champs demandés\n                                             │       reviews ?  pas demandé → skip\n                                             │       recos ?    pas demandé → skip\n                                             │       stock ?    pas demandé → skip\n                                             │\n                                             │ 2. FILTRE : réduit la réponse\n                                             │    aux seuls chemins demandés\n                                             ▼\nClient  ◄──  Product { title, price }  ──  Serveur\n",[18,387,385],{"__ignoreMap":55},[14,389,390,393,394,397],{},[33,391,392],{},"Tâche 1 : garder le travail coûteux."," C'est celle qui fait économiser, et le serveur doit la faire ",[26,395,396],{},"à la main",". FieldMask ne saute pas votre code par magie ; il vous dit seulement ce qu'il est sûr de sauter. En Go :",[50,399,403],{"className":400,"code":401,"language":402,"meta":55,"style":55},"language-go shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","func (s *server) GetProduct(ctx context.Context, req *pb.GetProductRequest) (*pb.Product, error) {\n    paths := req.GetFieldMask().GetPaths()\n    wants := func(f string) bool { return len(paths) == 0 || slices.Contains(paths, f) }\n\n    p, err := s.loadProductRow(ctx, req.GetId()) \u002F\u002F pas cher : id, title, price\n    if err != nil {\n        return nil, err\n    }\n\n    if wants(\"reviews\") {\n        p.Reviews = s.aggregateReviews(ctx, p.Id) \u002F\u002F coûteux, seulement si demandé\n    }\n    if wants(\"recommendations\") {\n        p.Recommendations = s.fetchRecommendations(ctx, p.Id) \u002F\u002F appel distant\n    }\n    if wants(\"stock\") {\n        p.Stock = s.fanOutStock(ctx, p.Id) \u002F\u002F fan-out\n    }\n\n    return p, nil\n}\n","go",[18,404,405,410,415,420,424,429,434,439,444,448,453,458,462,468,474,479,485,491,496,501,507],{"__ignoreMap":55},[59,406,407],{"class":61,"line":62},[59,408,409],{},"func (s *server) GetProduct(ctx context.Context, req *pb.GetProductRequest) (*pb.Product, error) {\n",[59,411,412],{"class":61,"line":68},[59,413,414],{},"    paths := req.GetFieldMask().GetPaths()\n",[59,416,417],{"class":61,"line":74},[59,418,419],{},"    wants := func(f string) bool { return len(paths) == 0 || slices.Contains(paths, f) }\n",[59,421,422],{"class":61,"line":80},[59,423,84],{"emptyLinePlaceholder":83},[59,425,426],{"class":61,"line":87},[59,427,428],{},"    p, err := s.loadProductRow(ctx, req.GetId()) \u002F\u002F pas cher : id, title, price\n",[59,430,431],{"class":61,"line":93},[59,432,433],{},"    if err != nil {\n",[59,435,436],{"class":61,"line":99},[59,437,438],{},"        return nil, err\n",[59,440,441],{"class":61,"line":105},[59,442,443],{},"    }\n",[59,445,446],{"class":61,"line":111},[59,447,84],{"emptyLinePlaceholder":83},[59,449,450],{"class":61,"line":117},[59,451,452],{},"    if wants(\"reviews\") {\n",[59,454,455],{"class":61,"line":123},[59,456,457],{},"        p.Reviews = s.aggregateReviews(ctx, p.Id) \u002F\u002F coûteux, seulement si demandé\n",[59,459,460],{"class":61,"line":129},[59,461,443],{},[59,463,465],{"class":61,"line":464},13,[59,466,467],{},"    if wants(\"recommendations\") {\n",[59,469,471],{"class":61,"line":470},14,[59,472,473],{},"        p.Recommendations = s.fetchRecommendations(ctx, p.Id) \u002F\u002F appel distant\n",[59,475,477],{"class":61,"line":476},15,[59,478,443],{},[59,480,482],{"class":61,"line":481},16,[59,483,484],{},"    if wants(\"stock\") {\n",[59,486,488],{"class":61,"line":487},17,[59,489,490],{},"        p.Stock = s.fanOutStock(ctx, p.Id) \u002F\u002F fan-out\n",[59,492,494],{"class":61,"line":493},18,[59,495,443],{},[59,497,499],{"class":61,"line":498},19,[59,500,84],{"emptyLinePlaceholder":83},[59,502,504],{"class":61,"line":503},20,[59,505,506],{},"    return p, nil\n",[59,508,510],{"class":61,"line":509},21,[59,511,77],{},[14,513,514,515,518,519,522],{},"Notez le repli ",[18,516,517],{},"len(paths) == 0"," : ",[33,520,521],{},"un masque vide veut dire « tout »",". Un client qui ne connaît pas ou se moque de FieldMask continue de fonctionner exactement comme avant. C'est ce qui rend la fonctionnalité sûre à ajouter sur une API existante.",[14,524,525,528,529,532],{},[33,526,527],{},"Tâche 2 : filtrer la réponse."," Même après la garde, un champ peut être rempli sans que le client l'ait demandé (valeurs par défaut, valeurs en cache, champ gratuit à calculer). Réduire le message au masque garde le contrat honnête et le payload léger. Le runtime protobuf fournit un utilitaire pour ça : en Java c'est ",[18,530,531],{},"FieldMaskUtil.merge()",", la plupart des langages ont un équivalent. Appliqué sur la réponse construite, il retire tout chemin absent du masque.",[42,534,536],{"id":535},"lécriture-cest-la-même-idée","L'écriture, c'est la même idée",[14,538,539,540,543],{},"FieldMask n'est pas que pour la lecture. Sur une mise à jour, le même masque répond à une autre question, ",[26,541,542],{},"« quels champs ai-je le droit de modifier ? »",", c'est exactement ce que veut dire PATCH en REST :",[50,545,547],{"className":52,"code":546,"language":54,"meta":55,"style":55},"message UpdateProductRequest {\n  Product product = 1;\n  google.protobuf.FieldMask update_mask = 2;\n}\n",[18,548,549,554,559,564],{"__ignoreMap":55},[59,550,551],{"class":61,"line":62},[59,552,553],{},"message UpdateProductRequest {\n",[59,555,556],{"class":61,"line":68},[59,557,558],{},"  Product product = 1;\n",[59,560,561],{"class":61,"line":74},[59,562,563],{},"  google.protobuf.FieldMask update_mask = 2;\n",[59,565,566],{"class":61,"line":80},[59,567,77],{},[14,569,570,571,574,575,578,579,582,583,590],{},"Avec ",[18,572,573],{},"update_mask { paths: \"price\" }",", le serveur ne touche ",[33,576,577],{},"qu'au"," prix et laisse tout le reste intact. Mais l'écriture a ses propres règles : effacer un champ ou le laisser tel quel, les chemins imbriqués, et le danger d'un masque vide (qui, en écriture, veut dire ",[26,580,581],{},"tout remplacer",", exactement l'inverse de la lecture). C'est un sujet à part entière, traité dans la suite : ",[33,584,585],{},[586,587,589],"a",{"href":588},"\u002Ffr\u002Fblog\u002Fbackend\u002Fapi-design\u002Fprotobuf-fieldmask-mutations","Protobuf FieldMask pour l'écriture →",".",[42,592,594],{"id":593},"les-pièges","Les pièges",[596,597,598,620,634,647],"ul",{},[599,600,601,604,605,608,609,612,613,615,616,619],"li",{},[33,602,603],{},"Les noms de champs deviennent une partie de votre contrat d'API."," Sur le fil, protobuf identifie les champs par ",[26,606,607],{},"numéro",", donc renommer un champ est normalement sans risque. Avec FieldMask, le client envoie le ",[26,610,611],{},"nom"," du champ sous forme de chaîne, donc renommez ",[18,614,178],{}," en ",[18,617,618],{},"review_summary"," et tous les masques existants cassent silencieusement. Règle : ne renommez pas un champ masqué. Si vous devez le faire, continuez d'accepter l'ancien nom côté serveur jusqu'à ce que tous les appelants aient migré, puis dépréciez-le.",[599,621,622,625,626,629,630,633],{},[33,623,624],{},"Le masque ne saute rien tout seul."," Ce n'est qu'une liste de chaînes. Si vous oubliez les gardes ",[18,627,628],{},"if wants(...)",", vous calculez tout et vous ne gagnez rien. Les économies vivent dans ",[26,631,632],{},"votre"," code, pas dans le framework.",[599,635,636,639,640,642,643,646],{},[33,637,638],{},"Les champs répétés ne se sous-sélectionnent pas."," Vous pouvez demander ",[18,641,192],{},", mais pas ",[18,644,645],{},"recommendations.title",", car le FieldMask de protobuf n'a pas de syntaxe pour « ce sous-champ, dans chaque élément d'une liste ». Connaissez la limite avant de concevoir autour.",[599,648,649,652,653,162,656,659],{},[33,650,651],{},"Livrez des masques par défaut sensés."," N'obligez pas chaque client à écrire ses chemins à la main. Exposez des masques prêts à l'emploi dans votre bibliothèque cliente pour les cas courants (",[18,654,655],{},"ProductMasks.SUMMARY",[18,657,658],{},"ProductMasks.FULL","), pour que les appelants choisissent un jeu soigné plutôt que de deviner des noms de champs.",[42,661,663],{"id":662},"récapitulatif","Récapitulatif",[136,665,666,676],{},[139,667,668],{},[142,669,670,673],{},[145,671,672],{},"Sans FieldMask",[145,674,675],{},"Avec FieldMask",[152,677,678,686,694,705],{},[142,679,680,683],{},[157,681,682],{},"Un endpoint calcule tout pour tout le monde",[157,684,685],{},"Chaque appelant déclare les champs qu'il veut",[142,687,688,691],{},[157,689,690],{},"Les appelants légers paient les champs coûteux",[157,692,693],{},"Le travail coûteux ne tourne que sur demande",[142,695,696,702],{},[157,697,698,699,701],{},"Un flag ",[18,700,268],{}," par champ coûteux",[157,703,704],{},"Un seul mécanisme, tout champ, imbrication incluse",[142,706,707,710],{},[157,708,709],{},"PATCH ambigu (vide = effacer ou ignorer ?)",[157,711,712],{},"Le masque de mise à jour dit exactement quoi changer",[14,714,715],{},"FieldMask est un ajout minuscule (un champ sur la requête) qui transforme un endpoint rigide et uniforme en un endpoint flexible, sans explosion de flags et sans casser les clients existants (masque vide = tout). Le hic : ça ne paie que si vous branchez les gardes vous-même. Le masque vous dit ce que vous avez le droit de sauter, mais sauter le travail reste votre boulot.",[717,718,719],"style",{},"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);}",{"title":55,"searchDepth":68,"depth":68,"links":721},[722,723,724,725,726,727,728],{"id":44,"depth":68,"text":45},{"id":224,"depth":68,"text":225},{"id":279,"depth":68,"text":280},{"id":373,"depth":68,"text":374},{"id":535,"depth":68,"text":536},{"id":593,"depth":68,"text":594},{"id":662,"depth":68,"text":663},"2026-07-03","Un seul endpoint, plein d'appelants, et des champs parfois très coûteux à produire. Protobuf FieldMask permet à chaque client de déclarer exactement les champs qu'il veut, pour que le serveur saute les requêtes SQL lourdes, les appels distants et le payload inutiles. L'idée, un exemple classique, et les pièges.",false,"md",null,{},"\u002Fbackend\u002Fapi-design\u002Fprotobuf-fieldmask.fr",{"title":5,"description":730},"protobuf-fieldmask-sparse-fieldsets","2.backend\u002F2.api-design\u002F1.protobuf-fieldmask.fr",[740,741,742,743],"gRPC","Protobuf","API-Design","Performance",[745],"backend","ACJgKHP7VzUws3VLtdX11gtR0odt_MTM5Ji-qpJJWGo",{"id":748,"title":749,"body":750,"date":729,"description":1355,"draft":731,"extension":732,"img":733,"meta":1356,"navigation":83,"path":1357,"seo":1358,"slug":737,"stem":1359,"tags":1360,"topics":1361,"__hash__":1362},"content\u002F2.backend\u002F2.api-design\u002F1.protobuf-fieldmask.md","Protobuf FieldMask: Let Your API Clients Ask for Only What They Need",{"type":7,"value":751,"toc":1346},[752,755,761,768,777,781,784,840,843,900,915,919,922,950,959,962,966,974,990,1006,1009,1029,1032,1037,1041,1048,1054,1064,1156,1166,1175,1179,1186,1206,1226,1230,1288,1292,1341,1344],[10,753,749],{"id":754},"protobuf-fieldmask-let-your-api-clients-ask-for-only-what-they-need",[14,756,757,758,760],{},"A single endpoint is rarely used by a single client. A mobile app, a web front end, a batch job and another backend service all hit the same ",[18,759,20],{},", and they don't all want the same thing. The mobile app needs a title and a price. The product page wants reviews and recommendations too. The batch job only needs an ID and a timestamp.",[14,762,763,764,767],{},"The problem: if the server always computes ",[26,765,766],{},"everything",", the cheapest caller pays the price of the most expensive one. Some fields are free (they're already in the row you fetched). Others are not: they mean a database aggregation, a call to another service, or a big payload over a slow mobile connection.",[14,769,770,772,773,776],{},[33,771,35],{}," is a small, standard way for the client to say ",[26,774,775],{},"\"return only these fields\"",", and for the server to skip the work behind the fields nobody asked for. This article explains the idea with a classic online-store API, not a copy-paste recipe, so you can apply it to your own services.",[42,778,780],{"id":779},"a-classic-scenario","A classic scenario",[14,782,783],{},"Say you run an online store. Your product service exposes one gRPC method:",[50,785,787],{"className":52,"code":786,"language":54,"meta":55,"style":55},"service ProductService {\n  rpc GetProduct(GetProductRequest) returns (Product);\n}\n\nmessage Product {\n  string id             = 1;\n  string title          = 2;\n  Money  price          = 3;\n  ReviewSummary reviews = 4;  \u002F\u002F aggregation over the reviews table\n  repeated Product recommendations = 5;  \u002F\u002F call to the reco service\n  StockInfo stock       = 6;  \u002F\u002F fan-out to every warehouse\n}\n",[18,788,789,793,797,801,805,809,813,817,821,826,831,836],{"__ignoreMap":55},[59,790,791],{"class":61,"line":62},[59,792,65],{},[59,794,795],{"class":61,"line":68},[59,796,71],{},[59,798,799],{"class":61,"line":74},[59,800,77],{},[59,802,803],{"class":61,"line":80},[59,804,84],{"emptyLinePlaceholder":83},[59,806,807],{"class":61,"line":87},[59,808,90],{},[59,810,811],{"class":61,"line":93},[59,812,96],{},[59,814,815],{"class":61,"line":99},[59,816,102],{},[59,818,819],{"class":61,"line":105},[59,820,108],{},[59,822,823],{"class":61,"line":111},[59,824,825],{},"  ReviewSummary reviews = 4;  \u002F\u002F aggregation over the reviews table\n",[59,827,828],{"class":61,"line":117},[59,829,830],{},"  repeated Product recommendations = 5;  \u002F\u002F call to the reco service\n",[59,832,833],{"class":61,"line":123},[59,834,835],{},"  StockInfo stock       = 6;  \u002F\u002F fan-out to every warehouse\n",[59,837,838],{"class":61,"line":129},[59,839,77],{},[14,841,842],{},"Look at the cost of each field:",[136,844,845,855],{},[139,846,847],{},[142,848,849,852],{},[145,850,851],{},"Field",[145,853,854],{},"Cost to produce",[152,856,857,870,882,891],{},[142,858,859,867],{},[157,860,861,162,863,162,865],{},[18,862,161],{},[18,864,165],{},[18,866,168],{},[157,868,869],{},"free, already in the product row",[142,871,872,876],{},[157,873,874],{},[18,875,178],{},[157,877,878,879,881],{},"a ",[18,880,184],{}," aggregation over millions of rows",[142,883,884,888],{},[157,885,886],{},[18,887,192],{},[157,889,890],{},"a remote call to the recommendation service",[142,892,893,897],{},[157,894,895],{},[18,896,202],{},[157,898,899],{},"a fan-out to every warehouse, then a sum",[14,901,902,903,905,906,908,909,911,912,914],{},"A phone showing a search result only needs ",[18,904,165],{}," and ",[18,907,168],{},". But if ",[18,910,20],{}," always builds the full ",[18,913,220],{},", that phone triggers a reviews aggregation, a reco call and a stock fan-out it will never display. Multiply by thousands of requests per second and you're burning CPU, saturating downstream services, and shipping bytes no one reads.",[42,916,918],{"id":917},"the-tempting-fix-that-doesnt-scale","The tempting fix that doesn't scale",[14,920,921],{},"The first instinct is boolean flags:",[50,923,924],{"className":52,"code":231,"language":54,"meta":55,"style":55},[18,925,926,930,934,938,942,946],{"__ignoreMap":55},[59,927,928],{"class":61,"line":62},[59,929,238],{},[59,931,932],{"class":61,"line":68},[59,933,243],{},[59,935,936],{"class":61,"line":74},[59,937,248],{},[59,939,940],{"class":61,"line":80},[59,941,253],{},[59,943,944],{"class":61,"line":87},[59,945,258],{},[59,947,948],{"class":61,"line":93},[59,949,77],{},[14,951,952,953,955,956,958],{},"It works, until it doesn't. Every new expensive field adds another ",[18,954,268],{}," flag. Nested fields (I want ",[18,957,272],{}," but not the full breakdown) have no clean expression. And every client has to know your private list of flags. It's a combinatorial mess that grows with your schema.",[14,960,961],{},"You want one mechanism that scales to any field, nests naturally, and is part of the standard toolbox. That's FieldMask.",[42,963,965],{"id":964},"what-fieldmask-actually-is","What FieldMask actually is",[14,967,968,970,971,973],{},[18,969,285],{}," is a ",[33,972,293],{}," shipped with Protocol Buffers. It's almost nothing, just a list of field paths:",[50,975,976],{"className":52,"code":297,"language":54,"meta":55,"style":55},[18,977,978,982,986],{"__ignoreMap":55},[59,979,980],{"class":61,"line":62},[59,981,304],{},[59,983,984],{"class":61,"line":68},[59,985,309],{},[59,987,988],{"class":61,"line":74},[59,989,77],{},[14,991,992,993,162,995,162,997,999,1000,1002,1003,1005],{},"A path is a field name, dot-separated for nesting: ",[18,994,165],{},[18,996,168],{},[18,998,272],{},". That's it. The client fills it in, the server reads it. It's the gRPC cousin of REST's ",[18,1001,326],{}," and of GraphQL's field selection, a ",[33,1004,333],{},": ask for a subset, get a subset.",[14,1007,1008],{},"You add one field to your request:",[50,1010,1011],{"className":52,"code":340,"language":54,"meta":55,"style":55},[18,1012,1013,1017,1021,1025],{"__ignoreMap":55},[59,1014,1015],{"class":61,"line":62},[59,1016,238],{},[59,1018,1019],{"class":61,"line":68},[59,1020,243],{},[59,1022,1023],{"class":61,"line":74},[59,1024,355],{},[59,1026,1027],{"class":61,"line":80},[59,1028,77],{},[14,1030,1031],{},"And the client asks for exactly what it needs:",[50,1033,1035],{"className":1034,"code":367,"language":368},[366],[18,1036,367],{"__ignoreMap":55},[42,1038,1040],{"id":1039},"reading-with-a-mask","Reading with a mask",[14,1042,1043,1044,1047],{},"Here's the flow. The mask does ",[33,1045,1046],{},"two distinct jobs",", and it's important not to confuse them:",[50,1049,1052],{"className":1050,"code":1051,"language":368},[366],"Client  ── field_mask { title, price } ──►  Server\n                                             │\n                                             │ 1. GUARD: only do the expensive\n                                             │    work for requested fields\n                                             │       reviews?   not asked → skip\n                                             │       recos?     not asked → skip\n                                             │       stock?     not asked → skip\n                                             │\n                                             │ 2. FILTER: trim the response\n                                             │    down to the requested paths\n                                             ▼\nClient  ◄──  Product { title, price }  ──  Server\n",[18,1053,1051],{"__ignoreMap":55},[14,1055,1056,1059,1060,1063],{},[33,1057,1058],{},"Job 1: guard the expensive work."," This is the one that saves money, and the server must do it ",[26,1061,1062],{},"by hand",". FieldMask does not magically skip your code; it just tells you what's safe to skip. In Go:",[50,1065,1067],{"className":400,"code":1066,"language":402,"meta":55,"style":55},"func (s *server) GetProduct(ctx context.Context, req *pb.GetProductRequest) (*pb.Product, error) {\n    paths := req.GetFieldMask().GetPaths()\n    wants := func(f string) bool { return len(paths) == 0 || slices.Contains(paths, f) }\n\n    p, err := s.loadProductRow(ctx, req.GetId()) \u002F\u002F cheap: id, title, price\n    if err != nil {\n        return nil, err\n    }\n\n    if wants(\"reviews\") {\n        p.Reviews = s.aggregateReviews(ctx, p.Id) \u002F\u002F expensive, only if asked\n    }\n    if wants(\"recommendations\") {\n        p.Recommendations = s.fetchRecommendations(ctx, p.Id) \u002F\u002F remote call\n    }\n    if wants(\"stock\") {\n        p.Stock = s.fanOutStock(ctx, p.Id) \u002F\u002F fan-out\n    }\n\n    return p, nil\n}\n",[18,1068,1069,1073,1077,1081,1085,1090,1094,1098,1102,1106,1110,1115,1119,1123,1128,1132,1136,1140,1144,1148,1152],{"__ignoreMap":55},[59,1070,1071],{"class":61,"line":62},[59,1072,409],{},[59,1074,1075],{"class":61,"line":68},[59,1076,414],{},[59,1078,1079],{"class":61,"line":74},[59,1080,419],{},[59,1082,1083],{"class":61,"line":80},[59,1084,84],{"emptyLinePlaceholder":83},[59,1086,1087],{"class":61,"line":87},[59,1088,1089],{},"    p, err := s.loadProductRow(ctx, req.GetId()) \u002F\u002F cheap: id, title, price\n",[59,1091,1092],{"class":61,"line":93},[59,1093,433],{},[59,1095,1096],{"class":61,"line":99},[59,1097,438],{},[59,1099,1100],{"class":61,"line":105},[59,1101,443],{},[59,1103,1104],{"class":61,"line":111},[59,1105,84],{"emptyLinePlaceholder":83},[59,1107,1108],{"class":61,"line":117},[59,1109,452],{},[59,1111,1112],{"class":61,"line":123},[59,1113,1114],{},"        p.Reviews = s.aggregateReviews(ctx, p.Id) \u002F\u002F expensive, only if asked\n",[59,1116,1117],{"class":61,"line":129},[59,1118,443],{},[59,1120,1121],{"class":61,"line":464},[59,1122,467],{},[59,1124,1125],{"class":61,"line":470},[59,1126,1127],{},"        p.Recommendations = s.fetchRecommendations(ctx, p.Id) \u002F\u002F remote call\n",[59,1129,1130],{"class":61,"line":476},[59,1131,443],{},[59,1133,1134],{"class":61,"line":481},[59,1135,484],{},[59,1137,1138],{"class":61,"line":487},[59,1139,490],{},[59,1141,1142],{"class":61,"line":493},[59,1143,443],{},[59,1145,1146],{"class":61,"line":498},[59,1147,84],{"emptyLinePlaceholder":83},[59,1149,1150],{"class":61,"line":503},[59,1151,506],{},[59,1153,1154],{"class":61,"line":509},[59,1155,77],{},[14,1157,1158,1159,1161,1162,1165],{},"Note the ",[18,1160,517],{}," fallback: ",[33,1163,1164],{},"an empty mask means \"everything\"",". A client that doesn't know or care about FieldMask keeps working exactly as before. That's what makes the feature safe to add to an existing API.",[14,1167,1168,1171,1172,1174],{},[33,1169,1170],{},"Job 2: filter the response."," Even after guarding, a field might be populated when the client didn't ask for it (defaults, cached values, a field that's free to compute). Trimming the message to the mask keeps the contract honest and the payload small. The protobuf runtime ships a helper for this: in Java it's ",[18,1173,531],{},"; most languages have an equivalent. Applied over the built response, it strips every path not in the mask.",[42,1176,1178],{"id":1177},"writes-use-the-same-idea","Writes use the same idea",[14,1180,1181,1182,1185],{},"FieldMask isn't only for reads. On an update, the same mask answers a different question, ",[26,1183,1184],{},"\"which fields am I allowed to change?\"",", which is exactly what PATCH means in REST:",[50,1187,1188],{"className":52,"code":546,"language":54,"meta":55,"style":55},[18,1189,1190,1194,1198,1202],{"__ignoreMap":55},[59,1191,1192],{"class":61,"line":62},[59,1193,553],{},[59,1195,1196],{"class":61,"line":68},[59,1197,558],{},[59,1199,1200],{"class":61,"line":74},[59,1201,563],{},[59,1203,1204],{"class":61,"line":80},[59,1205,77],{},[14,1207,1208,1209,1211,1212,1215,1216,1219,1220,590],{},"With ",[18,1210,573],{},", the server touches ",[33,1213,1214],{},"only"," the price and leaves the rest alone. But writes come with their own rules: clearing a field versus leaving it untouched, nested paths, and the danger of an empty mask (which on a write means ",[26,1217,1218],{},"replace everything",", the exact opposite of reads). That's a topic of its own, covered in the follow-up: ",[33,1221,1222],{},[586,1223,1225],{"href":1224},"\u002Fblog\u002Fbackend\u002Fapi-design\u002Fprotobuf-fieldmask-mutations","Protobuf FieldMask for writes →",[42,1227,1229],{"id":1228},"the-traps","The traps",[596,1231,1232,1252,1265,1277],{},[599,1233,1234,1237,1238,1241,1242,1245,1246,1248,1249,1251],{},[33,1235,1236],{},"Field names become part of your API contract."," On the wire, protobuf identifies fields by ",[26,1239,1240],{},"number",", so renaming a field is normally safe. With FieldMask the client sends the field ",[26,1243,1244],{},"name"," as a string, so rename ",[18,1247,178],{}," to ",[18,1250,618],{}," and every existing mask breaks silently. Rule: don't rename masked fields. If you must, keep accepting the old name in the server until every caller has migrated, then deprecate.",[599,1253,1254,1257,1258,1260,1261,1264],{},[33,1255,1256],{},"The mask doesn't skip anything by itself."," It's just a list of strings. If you forget the ",[18,1259,628],{}," guards, you compute everything and gain nothing. The savings live in ",[26,1262,1263],{},"your"," code, not in the framework.",[599,1266,1267,1270,1271,1273,1274,1276],{},[33,1268,1269],{},"Repeated fields can't be sub-selected."," You can ask for ",[18,1272,192],{},", but not ",[18,1275,645],{},", because protobuf FieldMask has no syntax for \"this sub-field, inside each element of a list.\" Know the limit before you design around it.",[599,1278,1279,1282,1283,162,1285,1287],{},[33,1280,1281],{},"Ship sensible default masks."," Don't make every client hand-write paths. Expose ready-made masks in your client library for the common cases (",[18,1284,655],{},[18,1286,658],{},") so callers opt into a curated set instead of guessing field names.",[42,1289,1291],{"id":1290},"recap","Recap",[136,1293,1294,1304],{},[139,1295,1296],{},[142,1297,1298,1301],{},[145,1299,1300],{},"Without FieldMask",[145,1302,1303],{},"With FieldMask",[152,1305,1306,1314,1322,1333],{},[142,1307,1308,1311],{},[157,1309,1310],{},"One endpoint computes everything for everyone",[157,1312,1313],{},"Each caller declares the fields it needs",[142,1315,1316,1319],{},[157,1317,1318],{},"Cheap callers pay for expensive fields",[157,1320,1321],{},"Expensive work runs only when requested",[142,1323,1324,1330],{},[157,1325,1326,1327,1329],{},"A new ",[18,1328,268],{}," flag per expensive field",[157,1331,1332],{},"One mechanism, any field, nesting included",[142,1334,1335,1338],{},[157,1336,1337],{},"PATCH is ambiguous (empty = clear or ignore?)",[157,1339,1340],{},"The update mask says exactly what changes",[14,1342,1343],{},"FieldMask is a tiny addition (one field on the request) that turns a rigid, one-size-fits-all endpoint into a flexible one, without a flag explosion and without breaking existing clients (empty mask = everything). The catch is that it only pays off if you wire the guards yourself: the mask tells you what you're allowed to skip, but skipping the work is still your job.",[717,1345,719],{},{"title":55,"searchDepth":68,"depth":68,"links":1347},[1348,1349,1350,1351,1352,1353,1354],{"id":779,"depth":68,"text":780},{"id":917,"depth":68,"text":918},{"id":964,"depth":68,"text":965},{"id":1039,"depth":68,"text":1040},{"id":1177,"depth":68,"text":1178},{"id":1228,"depth":68,"text":1229},{"id":1290,"depth":68,"text":1291},"One endpoint, many callers, some very expensive fields. Protobuf FieldMask lets each client declare exactly which fields it wants, so your server can skip the costly database queries, remote calls and payload it doesn't need. The idea, a classic example, and the traps.",{},"\u002Fbackend\u002Fapi-design\u002Fprotobuf-fieldmask",{"title":749,"description":1355},"2.backend\u002F2.api-design\u002F1.protobuf-fieldmask",[740,741,742,743],[745],"tOLMeBGNG8gbh7Ve8Gs7UqFqyTuIKoa8rKV6749vHIk",[1364,1367],{"title":1365,"path":1366},"Maîtriser NestJS : libérer la puissance des relations avec TypeORM et les bases SQL","\u002Fbackend\u002Fnest-js\u002Funlocking-the-power-of-relationships-with-typeorm.fr",{"title":1368,"path":1369},"Protobuf FieldMask pour l'écriture : un seul endpoint d'update, zéro ambiguïté","\u002Fbackend\u002Fapi-design\u002Fprotobuf-fieldmask-mutations.fr",[1371,1377,1392],{"path":1369,"title":1368,"description":1372,"date":1373,"tags":1374,"topics":1376},"Une seule méthode Update qui ne modifie que les champs nommés, sans endpoint par champ ni écrasement du reste. Comment l'update_mask apporte une sémantique PATCH propre à gRPC : mettre à jour, effacer, chemins imbriqués, et le piège du masque vide qui peut effacer des données en silence.","2026-07-07",[740,741,742,1375],"Mutations",[745],{"path":1378,"title":1379,"description":1380,"date":1381,"tags":1382,"topics":1389},"\u002Fdocker\u002Fdeploy-expressjs-api-using-docker.fr","Déployer une API Express.js avec Docker","Comment déployer une API REST Express.js avec Docker, avec un focus sur les performances MongoDB, les builds Docker multi-stage, Jest pour les tests, et Nginx comme reverse proxy.","2024-04-11",[1383,1384,1385,1386,1387,1388],"Express.JS","API","Node.js","Mongo","CI\u002FCD","Docker",[1390,745,1391],"docker","devops",{"path":1366,"title":1365,"description":1393,"date":1394,"tags":1395,"topics":1398},"Libère la puissance des relations de données avec NestJS, TypeORM et les bases SQL. Maîtrise l'art de construire des structures de données complexes et des interactions fluides. Idéal pour les développeurs NestJS expérimentés comme pour les débutants qui veulent créer des applications à la pointe.","2023-10-18",[1396,1397,1384],"Nest-JS","Mirco-service",[1399,745],"nest-js",1783585188651]