
Building a Blog CMS with Next.js Server Actions: A Practical CRUD Walkthrough
Most CRUD tutorials start by building a REST API and then a client to call it. Next.js Server Actions remove that whole middle layer for a lot of real cases. A form can call a server function directly, that function can talk to Supabase, and the page can revalidate without you writing a single route handler just to move data around.
TL;DR / Written for skimmers
A Server Action is just an async function marked with the use server directive, callable directly from a form or a client component. For a blog CMS, that means the create-post form can call a function that inserts a row in Supabase, with no fetch call and no API route in between. Listing posts is the easy part: a Server Component queries Supabase directly during render, no client-side loading state required for the initial view. That is one of the more underrated wins of the App Router for an admin-style CMS screen. A single Server Action can handle both create and update if the form includes an optional id field. When the id is present, the action runs an update, otherwise it runs an insert. That keeps the form component simple and avoids duplicating validation logic across two actions. Delete actions deserve the same scrutiny as writes. Confirming intent in the UI, checking ownership or role server-side, and only then issuing the delete keeps the action safe even if someone calls it directly.
Why Server Actions replace a REST layer for simple CRUD
A Server Action is just an async function marked with the use server directive, callable directly from a form or a client component. For a blog CMS, that means the create-post form can call a function that inserts a row in Supabase, with no fetch call and no API route in between.
This is not a shortcut that skips good practice. Validation, authorization and error handling still belong in that function, they just live closer to the data instead of being split across a route handler and a client-side fetch wrapper.
Reading and listing posts
Listing posts is the easy part: a Server Component queries Supabase directly during render, no client-side loading state required for the initial view. That is one of the more underrated wins of the App Router for an admin-style CMS screen.
Filtering by category or published status becomes a query parameter handled in the same server component, so the page stays a thin layer over a SQL query instead of accumulating client-side filtering logic.
Writing: create and update through the same action
A single Server Action can handle both create and update if the form includes an optional id field. When the id is present, the action runs an update, otherwise it runs an insert. That keeps the form component simple and avoids duplicating validation logic across two actions.
After the write succeeds, calling revalidatePath on the blog index and the post page is enough to keep the cached output in sync, without managing client-side cache invalidation by hand.
Deleting safely and revalidating
Delete actions deserve the same scrutiny as writes. Confirming intent in the UI, checking ownership or role server-side, and only then issuing the delete keeps the action safe even if someone calls it directly.
Once the row is gone, the same revalidatePath calls used for updates clear the stale cache, so the post disappears from the list immediately instead of waiting for the next deploy or a manual refresh.