Lesson [slug] routes and params introduced the awaited params prop, and the shape of that API is the one to carry forward.
Inside app/blog/[slug]/page.js you read the slug with const { slug } = await params, because params is a Promise in current Next.js. Server components await it before destructuring. The Express spelling from Backend with Node.js was req.params, read synchronously off the request object, so the mental model is the same even though the syntax is not.
This unit keeps that awaited, server-side flavour and adds the missing half: writing data instead of only reading it.
Writing data without an API detour
So far every page only read data. Now users need to create things. In Backend with Node.js the recipe was: build a POST /api/posts endpoint, then have the frontend fetch it with a JSON body. Next.js offers a shortcut called a server action: a function marked "use server" that the framework exposes to forms automatically.
// app/actions.js "use server"; export async function createPost(formData) { const title = formData.get("title"); await db.posts.insert({ title }); }
Decode it:
"use server"at the top of the file marks every export as a server action.- The function runs only on the server, so it can touch the database directly.
- It receives a
FormDataobject, the standard web API for form contents.
Wiring it to a form
// app/new-post/page.js import { createPost } from "../actions"; export default function NewPostPage() { return ( <form action={createPost}> <input name="title" /> <button type="submit">Publish</button> </form> ); }
- Instead of
action="/some/url", the form'sactionis the function itself. Next.js generates the network plumbing. - The
name="title"attribute is whatformData.get("title")reads. Names are the contract. - Notice there is no
onSubmit, noe.preventDefault(), nofetch. The page can even stay a server component.
Where a server action runs
In the form above, clicking Publish runs createPost on the server, never in the browser.
A "use server" function always executes server-side, and Next.js builds the network plumbing to make that happen. The form submission travels over the network as an HTTP request, the server receives it, and then runs createPost with the submitted FormData. Because the function's code never reaches the browser, it can safely use the database client, read secret environment variables, and trust its own logic.
That is the whole appeal. You get the ergonomics of passing a function to a form, with the security properties of a backend endpoint.
Field names are the contract
Suppose the input is renamed to <input name="headline" /> but the action still calls formData.get("title"). That call returns null.
FormData is keyed by the name attributes of the submitted fields, and nothing else. With no field named title in the submission, there is no entry to find, so get("title") reports null rather than throwing. Your action then happily inserts a post with a null title, which is exactly the kind of quiet bug that reaches production.
Keeping
nameattributes andformData.get()calls in sync is your responsibility. This is one of the strongest arguments for validating the parsed values at the top of every action before touching the database.