A Next.js App Router form posting to a Server Action must send a record id the user never types. Compare passing it as action={deleteNote.bind(null, id)} with rendering a hidden input named id: what does each do to the submitted payload and to the form's ability to submit without JavaScript?
answer
- extra values must ride the submission
- bound arguments come first
- FormData is always the last parameter
- hidden inputs are readable in view-source
- encoded is not the same as authorised
basics
~20 sBoth keep the form submittable without JavaScript. A bound argument arrives as the action's first parameter, ahead of the FormData, and travels as an opaque encoded value; a hidden input arrives inside FormData as plain text that is readable and editable in the page source. Neither is a security control.
solid answer
~40 sBoth survive with JavaScript off, because both are encoded into the real POST form the server renders — this is the reason `bind` is the recommended way to pass extra arguments rather than closing over the value in a click handler. The differences are in shape and visibility. With `deleteNote.bind(null, id)`, the action signature becomes `(id, formData)`: bound arguments come first, `FormData` last, and Next encodes closed-over values rather than exposing them as readable attributes. With `<input type="hidden" name="id">`, the value is a plain string sitting in the HTML that any user can read and edit, and it arrives via `formData.get('id')`. Neither one is a trust boundary — the action must still authorise the caller against that id server-side, since either value can be tampered with before it is posted.
code
tsx · 12 lines// app/notes/delete-form.tsx — Server Component
import { deleteNote } from './actions'
export function DeleteForm({ id }: { id: string }) {
const deleteWithId = deleteNote.bind(null, id)
return (
<form action={deleteWithId}>
<button type="submit">Delete</button>
</form>
)
}go deeper
Know the two ways to send a value the user never types — bind it to the action or render a hidden input — and that a hidden input's value is visible in the page source.
Explain the parameter order precisely: bound arguments first, FormData last. Note that both approaches still submit without JavaScript, while capturing the value in a click handler does not.
Argue the tradeoff and defend the boundary: bound values are encoded but still client-supplied, so the action authorises the id against the session before touching data, and nothing secret is ever bound.
Set the convention team-wide — arguments bound rather than smuggled through handlers, authorisation derived server-side by default — so that new mutations inherit both the no-JS baseline and the trust model without individual review.
## The constraint A form submission carries named form fields. That is all the browser knows how to send. So when an action needs a value that is not a field the user filled in — a record id, a list position, a redirect target — you need a way to smuggle it into that submission *without* reaching for JavaScript, or you have quietly traded away the no-JS path. There are three candidates. Two of them keep the property; one does not. ## Option 1 — bind the argument ```tsx import { deleteNote } from './actions' export function DeleteForm({ id }: { id: string }) { const deleteWithId = deleteNote.bind(null, id) return ( <form action={deleteWithId}> <button type="submit">Delete</button> </form> ) } ``` ```ts 'use server' export async function deleteNote(id: string, formData: FormData) { // bound arguments come first; FormData is always last } ``` The signature is the detail interviewers probe: **bound arguments are prepended, and `FormData` is always the final parameter.** Getting that order backwards is the classic bug, and it fails at runtime in a confusing way because both parameters are objects. The form still renders as a real POST form, with the bound value encoded into its hidden fields — so this works with JavaScript disabled exactly like a plain form. Next encodes values closed over by a Server Action rather than emitting them as human-readable attribute text, so a bound id does not sit in the page source as plain text the way a hidden input does. ## Option 2 — hidden input ```tsx <form action={deleteNote}> <input type="hidden" name="id" value={id} /> <button type="submit">Delete</button> </form> ``` Also fully progressive: it is exactly the mechanism forms have used since the 1990s. The value arrives as `formData.get('id')`, typed as a string (or `File`), so you convert and validate it yourself. Its distinguishing property is visibility — the id is right there in view-source, readable and trivially editable before submission. That visibility is sometimes desirable (debuggable, obvious, no framework magic) and sometimes not (leaks an internal identifier into a public page). ## Option 3 — capture it in a click handler ```tsx 'use client' <button onClick={() => deleteNote(id)}>Delete</button> ``` This is the one that silently costs you the property. The `id` now lives in a JavaScript closure that only exists once the component hydrates; before that, and forever if the bundle fails, the control does nothing. Nothing about the value itself is wrong — it is the *invocation* that has moved off the browser's native path. ## Neither option is a security boundary This is the point a strong answer volunteers. A hidden input is user-editable by definition. A bound argument is encoded, not authorised: it arrives from the client, over the wire, on a public POST endpoint. In both cases the action must check that *this* session is allowed to act on *that* id before it touches the database. Encoding changes who can casually read the value; it does not change who can cause the action to run. A related consequence: because bound values are serialised to the client, they must be serialisable, and they should not be secrets. Binding an internal API token into an action so the server can use it is the wrong shape — read it from the environment inside the action instead. ## Choosing Use `bind` as the default: the value is typed as a real function parameter rather than a stringly-typed lookup, and it does not clutter the HTML. Use a hidden input when the value is genuinely part of the form's data model, when you want it visible for debugging, or when several actions read the same field. Use neither pattern's client-handler cousin unless the control legitimately cannot be a form — and then be deliberate about it.
- What breaks if you write the action as `(formData, id)` and bind the id?The parameters arrive in the other order, so `formData` holds the bound id and `id` holds the `FormData` object. Because both are objects, TypeScript catches it only if the action is typed, and at runtime you get confusing failures like `formData.get is not a function`. Bound arguments are prepended; `FormData` is always last.
- Can you bind a value that is not serialisable, such as a database client?No. Bound arguments have to cross the network to the browser and back, so they must be serialisable by the same rules as any value crossing that boundary. Anything the action needs but the client must never hold — clients, tokens, connections — should be created or read inside the action body instead.
- If bound values are encoded, is it safe to bind a user's role or permission flag?No. Encoding stops casual reading; it does not make the value trustworthy or prevent the action from being invoked with different data. Authorisation decisions must be derived server-side inside the action from the session, never from something the client supplied — bound, hidden, or otherwise.
saying these in an interview costs you the question
- Says FormData is the first parameter when arguments are bound
- Treats a hidden input as tamper-proof because it is hidden
- Claims encoded bound arguments make an id trustworthy
- Thinks bind requires the form to be a Client Component
- Binds a secret or a database handle into the action