React Server Components explained for teams shipping real products
Server components are not 'SSR with extra steps'. They change what code ships to the browser at all — and that changes how you structure a codebase.
Server actions are async functions marked with "use server" that run on the server and can be called directly from client components or used as a form action. They remove the need to hand-write API routes for mutations, but each one is a publicly reachable endpoint and must perform its own authentication, authorisation, validation and rate limiting.
A server action is an async server-side function that the client can invoke directly, with Next.js handling the network call, serialisation and revalidation for you.
Mark a function with "use server" — either at the top of a module or inside an async function — and Next.js compiles a reference the browser can call. Passing it to a form's action attribute means the form submits natively without JavaScript, then upgrades to a fetch-based submission once hydrated.
// app/contact/actions.ts
"use server";
import { z } from "zod";
const Schema = z.object({
email: z.string().email(),
message: z.string().min(20).max(5000),
});
export async function submitEnquiry(_prev: State, formData: FormData): Promise<State> {
const parsed = Schema.safeParse(Object.fromEntries(formData));
if (!parsed.success) {
return { ok: false, errors: parsed.error.flatten().fieldErrors };
}
await sendEnquiry(parsed.data);
return { ok: true };
}Return errors as data. Throwing inside an action surfaces the nearest error boundary, which is right for unexpected failures and wrong for 'this email address is invalid'. Model expected failures as a returned state object and render them next to the field.
"use client";
import { useActionState } from "react";
export function EnquiryForm() {
const [state, action, pending] = useActionState(submitEnquiry, { ok: false });
return (
<form action={action}>
<input name="email" type="email" required aria-describedby="email-error" />
{state.errors?.email && <p id="email-error">{state.errors.email[0]}</p>}
<button disabled={pending}>{pending ? "Sending…" : "Send"}</button>
</form>
);
}Authenticate and authorise inside every action, validate all input, and rate limit anything a stranger can reach.
Next.js protects the transport with origin checks and encrypted action ids, which mitigates classic CSRF. It does not and cannot know whether the current user is allowed to delete invoice 4821. That is your code's job.
Server actions are an internal mutation mechanism for your own UI. Route handlers are your public API. Most production apps we build have both, and the boundary is obvious once you ask who the caller is.
After a successful mutation, call revalidateTag or revalidatePath inside the action so the server caches drop the stale entry, and Next.js refreshes the current route's client cache for form-driven submissions. For actions invoked programmatically from a client component, follow up with router.refresh() to update what the user is looking at.
For optimistic UI, useOptimistic gives you an immediate local update that reconciles when the action resolves — worth it for high-frequency interactions like toggling a like, unnecessary for a contact form.
Yes, when passed to a form's action attribute. The browser performs a normal form POST and Next.js runs the action server-side, which is why progressive enhancement is a genuine benefit rather than a talking point.
Yes — import it and await it inside the handler. You lose the no-JavaScript fallback, so prefer the form action attribute for anything that is really a form.
No meaningful difference; both are a server round trip. Actions add automatic serialisation and revalidation handling, which usually removes code rather than adding latency.
Read the client IP from headers() at the top of the action and check a counter in Redis or your database before doing any work. There is no built-in limiter, and unauthenticated actions need one.
Harshal Patel
Founder & Lead Engineer, ROVQIX
Harshal leads engineering at ROVQIX, where he has shipped production Next.js, Node.js and PostgreSQL systems for startups, SaaS teams and ecommerce brands. He writes about the trade-offs behind architecture decisions rather than the framework of the week.
ROVQIXdesigns and builds production web platforms — Next.js front ends, Node.js APIs and the infrastructure behind them. Tell us what you're building and we'll scope it with you.
Server components are not 'SSR with extra steps'. They change what code ships to the browser at all — and that changes how you structure a codebase.
Not a compliance document. This is the list we actually work through before a client site handles its first real user.
Rate limiting is not just abuse prevention. It is the mechanism that stops one client's bad afternoon from becoming everyone's outage.
No spam. Just the occasional case study and craft breakdown.