Server Actions: mutating data without an API route
Part 13's Route Handler is one way to mutate data — a real endpoint, called with fetch from the client. A Server Action is a different shape entirely: a plain async function that runs on the server, callable directly from a form, with no API route or client-side fetch call written at all.
A minimal Server Action
// app/actions.ts
"use server";
export async function likePost(slug: string) {
console.log(`Liked: ${slug}`);
// a real implementation would update a database here
}"use server" at the top of the file — the mirror image of "use client" from part 3 — marks every exported function in it as a Server Action. Next.js compiles a callable reference to it that a Client Component can invoke directly; under the hood, that call becomes a network request to the server automatically, without a route.ts file or a hand-written fetch anywhere.
Calling it from a form
// app/components/LikeButton.tsx
"use client";
import { likePost } from "@/app/actions";
export function LikeButton({ slug }: { slug: string }) {
return (
<form action={() => likePost(slug)}>
<button type="submit">♡ Like</button>
</form>
);
}A <form>'s action prop accepting a function directly — not a URL string — is a React feature Server Actions build on. Submitting the form calls likePost(slug) on the server; no onSubmit handler, no event.preventDefault(), no manual fetch call constructing a request body.
Server Actions work without JavaScript, too
Because the underlying mechanism is still a real form submission, this specific pattern degrades gracefully — a form wired to a Server Action still works even if the page's JavaScript hasn't loaded yet (or fails to), since the browser's native form submission is what's actually driving it underneath. A client-side-only onClick handler calling fetch has no equivalent fallback; if the JavaScript never runs, nothing happens at all.
Revalidating stale data after a mutation
// app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function likePost(slug: string) {
// update the database
revalidatePath(`/posts/${slug}`);
}A page fetched with caching (part 5's revalidate option, part 17 in full) doesn't automatically know its underlying data just changed. revalidatePath tells Next.js to treat that specific page's cached data as stale immediately, so the next visit re-fetches fresh data instead of serving a now-outdated cached version — the standard pairing with any Server Action that changes something a cached page depends on.
Treating a Server Action like a Route Handler and manually calling fetch("/api/...") to invoke it from client code. A Server Action is called directly, as a plain function reference — likePost(slug), not a URL — Next.js's build step is what turns that direct call into the actual network request; writing fetch around it does nothing useful and adds an unnecessary layer.
Next: putting this together for something the blog actually needs — a real comment form, complete with validation and a pending state while it submits.