[{"data":1,"prerenderedAt":309},["ShallowReactive",2],{"blog-en-nextjs-14-server-actions":3},{"id":4,"title":5,"body":6,"cover":294,"date":295,"description":296,"draft":297,"extension":298,"locale":299,"meta":300,"navigation":138,"path":301,"seo":302,"stem":303,"tags":304,"__hash__":308},"blog\u002Fblog\u002Fen\u002Fnextjs-14-server-actions.md","Next.js 14: server actions in practice",{"type":7,"value":8,"toc":290},"minimark",[9,13,18,26,248,259,265,269,280,283,286],[10,11,12],"p",{},"A year ago I wrote about the App Router and called much of it exciting but unfinished. With Next.js 14, one piece of it has grown up: server actions are now stable. I've put them to work in real projects over the past weeks, and they change how I think about data mutations.",[14,15,17],"h2",{"id":16},"mutations-without-the-detour","Mutations without the detour",[10,19,20,21,25],{},"Until now, every write operation went through an API route. A form sent its data to an endpoint, which validated, wrote to the database, and responded — and in the end I maintained two separate worlds: the front end and the API between them. Server actions close that gap. I write a function, mark it with ",[22,23,24],"code",{},"\"use server\"",", and it's guaranteed to run on the server.",[27,28,33],"pre",{"className":29,"code":30,"language":31,"meta":32,"style":32},"language-jsx shiki shiki-themes github-light github-dark","async function save(formData) {\n  \"use server\";\n  const name = formData.get(\"name\");\n  await db.contact.create({ data: { name } });\n  revalidatePath(\"\u002Fcontacts\");\n}\n\nexport default function Form() {\n  return (\n    \u003Cform action={save}>\n      \u003Cinput name=\"name\" \u002F>\n      \u003Cbutton type=\"submit\">Save\u003C\u002Fbutton>\n    \u003C\u002Fform>\n  );\n}\n","jsx","",[22,34,35,62,72,99,114,127,133,140,157,166,185,203,227,237,243],{"__ignoreMap":32},[36,37,40,44,47,51,55,59],"span",{"class":38,"line":39},"line",1,[36,41,43],{"class":42},"szBVR","async",[36,45,46],{"class":42}," function",[36,48,50],{"class":49},"sScJk"," save",[36,52,54],{"class":53},"sVt8B","(",[36,56,58],{"class":57},"s4XuR","formData",[36,60,61],{"class":53},") {\n",[36,63,65,69],{"class":38,"line":64},2,[36,66,68],{"class":67},"sZZnC","  \"use server\"",[36,70,71],{"class":53},";\n",[36,73,75,78,82,85,88,91,93,96],{"class":38,"line":74},3,[36,76,77],{"class":42},"  const",[36,79,81],{"class":80},"sj4cs"," name",[36,83,84],{"class":42}," =",[36,86,87],{"class":53}," formData.",[36,89,90],{"class":49},"get",[36,92,54],{"class":53},[36,94,95],{"class":67},"\"name\"",[36,97,98],{"class":53},");\n",[36,100,102,105,108,111],{"class":38,"line":101},4,[36,103,104],{"class":42},"  await",[36,106,107],{"class":53}," db.contact.",[36,109,110],{"class":49},"create",[36,112,113],{"class":53},"({ data: { name } });\n",[36,115,117,120,122,125],{"class":38,"line":116},5,[36,118,119],{"class":49},"  revalidatePath",[36,121,54],{"class":53},[36,123,124],{"class":67},"\"\u002Fcontacts\"",[36,126,98],{"class":53},[36,128,130],{"class":38,"line":129},6,[36,131,132],{"class":53},"}\n",[36,134,136],{"class":38,"line":135},7,[36,137,139],{"emptyLinePlaceholder":138},true,"\n",[36,141,143,146,149,151,154],{"class":38,"line":142},8,[36,144,145],{"class":42},"export",[36,147,148],{"class":42}," default",[36,150,46],{"class":42},[36,152,153],{"class":49}," Form",[36,155,156],{"class":53},"() {\n",[36,158,160,163],{"class":38,"line":159},9,[36,161,162],{"class":42},"  return",[36,164,165],{"class":53}," (\n",[36,167,169,172,176,179,182],{"class":38,"line":168},10,[36,170,171],{"class":53},"    \u003C",[36,173,175],{"class":174},"s9eBZ","form",[36,177,178],{"class":49}," action",[36,180,181],{"class":42},"=",[36,183,184],{"class":53},"{save}>\n",[36,186,188,191,194,196,198,200],{"class":38,"line":187},11,[36,189,190],{"class":53},"      \u003C",[36,192,193],{"class":174},"input",[36,195,81],{"class":49},[36,197,181],{"class":42},[36,199,95],{"class":67},[36,201,202],{"class":53}," \u002F>\n",[36,204,206,208,211,214,216,219,222,224],{"class":38,"line":205},12,[36,207,190],{"class":53},[36,209,210],{"class":174},"button",[36,212,213],{"class":49}," type",[36,215,181],{"class":42},[36,217,218],{"class":67},"\"submit\"",[36,220,221],{"class":53},">Save\u003C\u002F",[36,223,210],{"class":174},[36,225,226],{"class":53},">\n",[36,228,230,233,235],{"class":38,"line":229},13,[36,231,232],{"class":53},"    \u003C\u002F",[36,234,175],{"class":174},[36,236,226],{"class":53},[36,238,240],{"class":38,"line":239},14,[36,241,242],{"class":53},"  );\n",[36,244,246],{"class":38,"line":245},15,[36,247,132],{"class":53},[10,249,250,251,254,255,258],{},"The form's ",[22,252,253],{},"action"," attribute takes the function directly. No ",[22,256,257],{},"fetch",", no endpoint, no manual serializing. And the lovely part: it works even without JavaScript in the browser, because it builds on the web standard for forms. Progressive enhancement, without me working for it.",[260,261,262],"blockquote",{},[10,263,264],{},"The best abstraction is the one that makes an entire layer unnecessary — not the one that adds another.",[14,266,268],{"id":267},"what-holds-up-in-practice","What holds up in practice",[10,270,271,272,275,276,279],{},"In real projects I value ",[22,273,274],{},"revalidatePath"," and ",[22,277,278],{},"revalidateTag"," most. After a mutation I tell Next.js precisely which data is stale, and the cache refreshes itself. No manual refetching, no orphaned state in the client.",[10,281,282],{},"Honestly, it took some discipline to draw the line cleanly. Server actions aren't a cure-all — for complex, multi-step flows or public interfaces I still reach for real endpoints. And validation belongs firmly on the server, because the client lies on principle.",[10,284,285],{},"But for what I build most often — forms, small mutations, the everyday life of an application — server actions feel right. They remove a layer I never loved. That's the kind of progress I like: less code that says more.",[287,288,289],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}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":32,"searchDepth":64,"depth":64,"links":291},[292,293],{"id":16,"depth":64,"text":17},{"id":267,"depth":64,"text":268},null,"2023-10-26","With Next.js 14, server actions are stable — and I can finally write mutations without separate API routes, right where they're needed.",false,"md","en",{},"\u002Fblog\u002Fen\u002Fnextjs-14-server-actions",{"title":5,"description":296},"blog\u002Fen\u002Fnextjs-14-server-actions",[305,306,307],"Next.js","React","Web","4tYAJai23DOzSBoO1vvPGzivf5NlR7JPmcAO5oGh5eA",1781691288101]