
Next.js use server
If you've written a Server Action in Next.js before, you've already used use server without necessarily thinking hard about what it does. It's easy to treat it as boilerplate — a magic string you paste at the top of a function so form submissions work. But use server isn't a Next.js convention bolted on top of React; it's a React directive that Next.js implements, and understanding exactly what it marks, where it can go, and what it doesn't protect you from will save you from a specific category of bug that only shows up once real users start sending real (and sometimes malicious) input to your app.
This article is deliberately narrow. It's not another walkthrough of building a form with a Server Action, and it's not the deep dive into the request/response model behind Server Actions — I've covered both of those elsewhere on this blog. This is the API reference for the directive itself: what it is, the two places you're allowed to put it, what qualifies as a valid Server Function once you do, and the serialization rules that govern what can actually cross the wire.
What use server Actually Marks
use server is a React directive, not a Next.js one — that distinction matters because it means the rules come from React's RSC (React Server Components) specification, and Next.js is simply the framework wiring those rules into a working request/response cycle. When you write use server, you're telling the React compiler and bundler: "the function(s) below this marker execute only on the server, never in the browser bundle."
That's the entire job of the directive. It doesn't add authentication. It doesn't add rate limiting. It doesn't validate input. It draws a boundary and tells the build tooling which side of that boundary a piece of code lives on — everything else is your responsibility, which is exactly why the security section below isn't optional reading.
The functions you mark this way are called Server Functions in React's terminology (Next.js calls the ones triggered from a form or event handler "Server Actions" — same underlying mechanism, slightly different name depending on context). They can be called from Server Components, Client Components, and even other Server Functions, and no matter where the call originates, the function body only ever runs on the server.
Two Places You Can Put It
use server works in exactly two positions, and picking the right one for the situation is mostly about how many functions you're marking and where those functions need to be visible from.
At the top of a file
Put 'use server' as the first line of a file, and every exported function in that file becomes a Server Function.
"use server";
import { db } from "@/lib/db";
import { auth } from "@/lib/auth";
export async function createUser(data: { name: string; email: string }) {
const session = await auth();
if (!session?.user) {
throw new Error("Unauthorized");
}
const user = await db.user.create({ data });
return { id: user.id, name: user.name };
}
This is the pattern you reach for when Server Functions need to be imported into a Client Component. A Client Component can't define its own Server Function inline (it can't contain server-only code at all), so the functions have to live in a separate file that Client Components import from:
"use client";
import { fetchUsers } from "../actions";
export default function MyButton() {
return <button onClick={() => fetchUsers()}>Fetch Users</button>;
}
Notice what's happening at the module boundary here: app/actions.ts never ships to the browser. When the Client Component imports fetchUsers, the bundler doesn't inline the function body — it replaces the import with a reference that, when called, triggers a network request to the server and invokes the real function there. The Client Component's bundle only ever contains a thin, callable stub.
Inline, at the top of a function
The second placement is inside a function body, as its first line — usually a function defined directly inside a Server Component:
import { EditPost } from "./edit-post";
import { revalidatePath } from "next/cache";
export default async function PostPage({ params }: PageProps<"/posts/[id]">) {
const { id } = await params;
const post = await getPost(id);
async function updatePost(formData: FormData) {
"use server";
// Verify auth before saving (e.g. inside savePost)
await savePost(id, formData);
revalidatePath(`/posts/${id}`);
}
return <EditPost updatePostAction={updatePost} post={post} />;
}
This pattern only marks the one function it's inside — nothing else in the file is affected. It's the natural choice when a Server Function is tightly scoped to a single Server Component and doesn't need to be reused or imported anywhere else. Notice that updatePost closes over id from the surrounding component's scope — that's a real, useful capability of the inline form that the file-level form doesn't give you as naturally, since a standalone actions file has no enclosing component to capture variables from.
A rule of thumb I use: if a Server Function needs to be called from a Client Component, it has to go in its own file with a file-level directive — there's no way around that, since the Client Component can only import, never define. If it's only ever invoked from within one Server Component and benefits from closing over that component's local variables, inline is simpler and keeps related code physically together.
What Makes a Function a Valid Server Function
Not every function you slap use server onto will work. There are a few structural requirements that are easy to violate without any obvious error message pointing you at the directive itself:
- It must be
async. Server Functions are invoked over the network — even when React is smart enough to skip an actual round-trip in some cases, the calling convention assumes a Promise-returning function. A synchronous function marked withuse serverisn't a supported shape. - File-level exports must be functions, not arbitrary values. If you put
'use server'at the top of a file, every top-level export from that file needs to be a Server Function. You can't mix in a plain exported constant or a type — the whole file is treated as a server functions module. - Arguments and return values must be serializable. This is the one that trips people up most often, and it deserves its own section.
The Serialization Boundary
Because a Server Function call actually crosses a network boundary (client → server) even when the "client" is prerendered HTML that hasn't hydrated yet, everything passed in and everything passed back has to survive being turned into a wire format and reconstructed on the other side.
React's Server Functions serialization supports the values you'd expect from JSON.stringify, plus a handful of extensions the RSC protocol adds on top: strings, numbers, booleans, null, undefined, plain objects and arrays, Date, Map, Set, FormData, and — notably — Promise values, which get streamed to the client as they resolve rather than requiring the whole response to wait.
What it does not support: class instances (other than the built-ins above), functions (with the specific exception of references to other Server Functions), and Symbol values that aren't globally registered. If you pass a Prisma model instance straight through as a return value, for example, you may find it silently loses its prototype methods on the client side, or errors outright, because it isn't a plain object — this is a common source of confusion since the object often looks fine when you log it on the server.
The practical fix is the same one experienced backend developers already default to: shape your return values explicitly rather than passing ORM results straight through.
// Fragile — passes a live database record across the boundary
export async function createUser(data: { name: string; email: string }) {
const user = await db.user.create({ data });
return user; // a Prisma model instance, not a plain object
}
// Robust — returns a plain, serializable shape
export async function createUser(data: { name: string; email: string }) {
const user = await db.user.create({ data });
return { id: user.id, name: user.name };
}
This isn't just a serialization workaround, either — it's also the right security posture, which brings us to the part of this directive that actually matters most.
Security Is Not Automatic
Here's the mental model shift that catches people who are new to Server Functions: marking a function use server does not make it private or trusted. It makes it reachable over the network as its own endpoint. Anyone who can see your rendered page can, in principle, construct a request that invokes that function directly — bypassing your UI, your form validation, your onClick handler, all of it.
Next.js and React give you the mechanism (code stays on the server, arguments and results are serialized safely); they do not give you authentication, authorization, or input validation. Those are entirely on you, inside the function body, every single time.
Authenticate from the request, not from arguments
A subtle but critical detail: read who the caller is from cookies or headers via your auth library, not from a parameter the client passed in.
"use server";
import { db } from "@/lib/db";
import { auth } from "@/lib/auth";
export async function createUser(data: { name: string; email: string }) {
const session = await auth();
if (!session?.user) {
throw new Error("Unauthorized");
}
const newUser = await db.user.create({ data });
return { id: newUser.id, name: newUser.name };
}
If instead you accepted something like userId: string as a parameter and trusted it to identify the caller, anyone could call the function with a different user's ID and act on their behalf — the parameter is just data from an untrusted client, no different from a query string. auth() reads the actual session cookie, which the browser can't forge without the real session.
Constrain what you return
The same discipline applies on the way out. Don't return a full database record because it's convenient — return exactly the fields the UI needs. This is partly the serialization concern from above, but it's also a data-exposure concern: a raw record might include a password hash, an internal-only flag, or another user's data joined in through a relation you forgot was there.
Centralize the guarantees with a Data Access Layer
If you have more than a couple of Server Functions touching the same tables, scattering auth() checks across every action file gets error-prone fast — it only takes one function where you forget the check. A Data Access Layer (a dedicated module that owns all reads and writes to a given resource, with authorization baked into every function it exports) means your Server Functions become thin wrappers that call into a layer where the guarantees already live in one place. If you're building anything beyond a toy project, this pattern is worth adopting early rather than retrofitting later.
Common Mistakes
Assuming file-level use server implies "private." Every export in that file is a public network endpoint the moment the directive is present. Treat it exactly as you would a REST API route with no auth middleware yet.
Skipping the async keyword. A non-async function with use server isn't a supported shape — React expects to be able to await the call.
Passing ORM records or class instances through the boundary. They aren't guaranteed to survive serialization intact, and even when they technically work, you're leaking more data than the client needs.
Forgetting that arguments are attacker-controlled. Anything a Server Function receives — form data, an ID, a flag — arrived from the client and should be treated with the same suspicion you'd apply to a raw HTTP request body, because that's functionally what it is.
Key Takeaways
| Question | Answer |
|---|---|
Where can use server go? | Top of a file (marks every export) or as the first line inside a function (marks just that one) |
| When must it be file-level? | When the function needs to be imported into a Client Component |
| When is inline better? | When the function only runs from one Server Component and benefits from closing over its local variables |
| What must the function look like? | async, with serializable arguments and a serializable return value |
| What does the directive NOT give you? | Authentication, authorization, input validation — all still your responsibility |
| Where should auth checks read from? | Cookies/headers via your auth library, never from a client-supplied parameter |
use server is a small directive doing a specific, limited job: it draws the line between code that runs on the server and code that ships to the browser. Everything people actually get hurt by — unauthenticated mutations, leaked data, spoofed identities — comes from treating that boundary as a security boundary when it's really just an execution boundary. Keep that distinction straight and the directive itself stops being something to worry about.


