In a Next.js App Router project, what is the difference between putting 'use server' on the first line of a module and putting it as the first statement inside a single async function?
answer
- file-wide versus one function
- every export becomes an endpoint
- inline can capture render scope
- async-only either way
- bind() is the explicit capture
basics
~20 sModule-level 'use server' turns every export of that file into a separately callable server endpoint; the inline form marks only the one async function it sits in. Inline actions can also close over values from the surrounding render scope, which module-level ones cannot.
solid answer
~50 sThe two forms differ in **scope** and in **what the function can capture**. At the top of a module, the directive applies to the whole file: every export becomes a server function with its own generated id, so adding an export to `actions.ts` publishes a new callable endpoint whether you meant to or not, and every export must be `async`. Inside a function body, it marks just that function — typically one defined in a Server Component and handed to a form or passed down to a Client Component as a prop — so nothing else in the file is exposed. The inline form can also close over variables from the enclosing render, which is why it is the natural choice for per-row or per-request actions; the module form has no render scope to capture, so everything it needs must arrive as an argument. In practice: shared mutations go in a `'use server'` module, one-off mutations that need render context go inline.
code
typescript · 12 lines// app/actions.ts — module form: BOTH exports are callable endpoints
'use server'
export async function createPost(formData: FormData) {
const title = String(formData.get('title') ?? '')
return { ok: title.length > 0 }
}
// exported only so another module can reuse it — still a public endpoint
export async function slugify(input: string) {
return input.toLowerCase().replace(/\s+/g, '-')
}go deeper
Know that the directive can sit at the top of a file or inside one async function, and that the top-of-file form covers every export of that file. Be able to point at each form in a code sample.
Explain both differences that matter: how many endpoints each form publishes, and the fact that only the inline form can capture values from the render around it. Mention the async-only rule for exports.
Argue placement as a design choice — keeping the generated endpoint surface small, moving helpers out of action modules, and preferring an explicit bound argument over an incidental closure so the captured data is visible in review.
Set the convention for a codebase: where action modules live, what may be exported from them, when a mutation graduates to a route handler instead, and how the team reviews growth of the generated RPC surface over time.
## The two placements `'use server'` is legal in exactly two positions, and they mean different things. **Module level** — the directive is the first statement of the file, before any import: ```tsx // app/actions.ts 'use server' export async function createPost(formData: FormData) { /* ... */ } export async function deletePost(id: string) { /* ... */ } ``` Every export of this file is now a server function. Each gets its own build-time id, and each is independently callable by anything that can send a POST to your app. **Inline** — the directive is the first statement inside an async function body: ```tsx // app/posts/[id]/page.tsx (a Server Component) export default async function Page({ params }: { params: Promise<{ id: string }> }) { const { id } = await params async function deleteThisPost() { 'use server' await db.post.delete({ where: { id } }) // captures id from the render above } return <form action={deleteThisPost}><button>Delete</button></form> } ``` Only `deleteThisPost` is a server function. The component around it is not; the module is not. ## Difference one: how much surface you publish This is why interviewers ask the question. A `'use server'` module is a *directory of endpoints*. Six exports mean six endpoints. There is no notion of an "internal" export in such a file — if it is exported, it is reachable. That has two everyday consequences: - A helper you exported only so a sibling module could import it is now a public POST target. - Every export must be an `async` function. You cannot keep a shared constant, a schema object, or a synchronous formatter in the same file; the build rejects it. The inline form publishes exactly one endpoint per marked function and nothing else, so an inline action inside a Server Component adds no surface beyond itself. ## Difference two: what the function can see A module-level action is defined at module scope. There is no request or render in scope when the module is evaluated, so everything it operates on has to come in through its parameters — the `FormData`, an id, whatever the caller passes. An inline action is defined *during a render*, so it can close over the values the component already computed: the route params, a row from a list, a locale. That is what makes the per-item pattern natural: ```tsx {posts.map((post) => { const remove = async () => { 'use server' await db.post.delete({ where: { id: post.id } }) } return <form key={post.id} action={remove}><button>Delete</button></form> })} ``` The capture is not free. Closed-over values do not stay on the server: they are serialized into the payload sent to the browser and sent back with the call, so they must be serializable, and you should not close over anything you would not hand to the client. The explicit spelling of the same idea is `deletePost.bind(null, post.id)` on a module-level action, which is often clearer and keeps the mutation in one place. ## Difference three: how a Client Component reaches it A Client Component cannot declare either form itself. It gets a server function by importing it from a `'use server'` module, or by receiving one as a prop from a Server Component. So the module form is what you reach for when many client components across the app need the same mutation; the inline form is what you reach for when the action belongs to one place in one tree. ## Choosing between them - Reused by several routes or several client components → module. One place to find it, one place to change it. - Needs values from the render that produced the UI → inline, or module plus `.bind`. - Long list of small mutations → a module, but keep it small and export only what is genuinely an action; move helpers, constants and schemas into a neighbouring file that has no directive. - Either way, the function is `async`, and the fact that only your UI calls it is not a property the runtime enforces. Both forms have behaved this way since Server Actions became stable in Next.js 14, and the description here matches Next.js 16.
- You need a per-row delete but want the mutation defined once in a shared module. How do you pass the row id without an inline action?Bind it: `deletePost.bind(null, post.id)` produces a callable you can hand to a form's `action`. The bound argument is serialized into the client payload and arrives as the action's first parameter at call time — the same transport as a closure, but the mutation itself stays in one reviewable module.
- Does the inline form give the function any privacy — is it harder to call than a module-level export?No. Both get a build-time id, and an id is all a caller needs. Inline placement limits how many endpoints exist, not who can reach the one that does. Any guarantee an action needs has to be established inside its own body.
- Why can't a module marked 'use server' export a plain constant alongside its actions?Because the directive turns every export into a callable endpoint, and only an async function can be represented that way. Next fails the build rather than silently ignoring the export. Shared constants and schemas belong in a separate module with no directive, imported by the action file.
saying these in an interview costs you the question
- Thinks the inline form hides the action from outside callers
- Says module-level applies only to the default export
- Believes a module-level action can read the current render's variables
- Puts helpers and constants in the same 'use server' file
- Assumes closed-over values stay on the server