Building Forms in Next.js with Server Actions

Build a Next.js form that submits to a Server Action: bind the action, read FormData, validate on the server, and show the returned result.

6 min read

Next.js forms with Server Actions submit directly to an async function that runs on the server. That function is a Server Action, and the pattern removes the client-side handler and fetch call a classic React form needs. Because the form is plain HTML, it still works before JavaScript loads.

Start with the action. It receives the submitted fields as a FormData object and reads them by name. The export is marked with use server so the function only ever runs on the server.

typescripttypescript
// app/actions.ts
'use server'
 
export async function addComment(formData: FormData) {
  const message = String(formData.get('message') ?? '')
  console.log('New comment:', message)
}

The function takes that object and reads one field from it. The log appears in the terminal running the dev server, not in the browser console, because none of this code ships to the browser.

Now attach it to a form. This page is a Server Component, so the form element renders on the server while the action prop points at the exported function.

App.tsxApp.tsx
// app/page.tsx
import { addComment } from './actions'
export default function CommentForm() {
  return <form action={addComment}>
    <label htmlFor="message">Message</label>
    <input id="message" name="message" required />
    <button type="submit">Send</button>
  </form>
}

When a visitor submits, the browser sends the field and the action runs on the server. The label, required attribute, and button all keep their normal HTML behavior.

What the action prop does

The action prop replaces the usual onSubmit handler plus fetch call. When the form submits, React encodes the fields and invokes the action, which then runs on the server with access to your database and secrets.

Two properties matter most. First, progressive enhancement: a form with an action prop still submits when JavaScript is disabled, because the browser can post the form itself. Second, the action never runs in the browser, so server-only code stays server-only.

The two paths differ in what the browser does. Before hydration the browser posts the form itself and loads the response as a normal navigation, and after hydration React sends the fields in the background and the page updates in place.

Validate on the server

The required attribute stops an empty submission in the browser, but it does not protect the action. The action is reachable over HTTP and can be called without the form, so repeat the check inside the action before anything is written.

typescripttypescript
// app/actions.ts
'use server'
import { db } from '@/lib/db'
 
export async function addComment(formData: FormData) {
  const message = String(formData.get('message') ?? '').trim()
  if (!message) return
  await db.comment.create({ data: { message } })
}

The guard runs before the write, so an empty submission stores nothing. The page reloads unchanged and the visitor sees the form again, with no error message yet.

Validation is only half of the check. Rendering the form behind a login does not protect the action, so read the session inside the action too and reject the request when the visitor is not allowed to write. See securing Server Actions for the auth and validation pattern together.

Schema-based validation scales better as fields grow. See server-side form validation with Zod for the schema approach.

Show the returned result

Neither action above returns anything, and that is deliberate: a plain form ignores whatever an action returns. To surface a message, turn the form into a Client Component and read the state from useActionState.

Passing a Server Action to the hook gives you three things: the action's last return value as state, a wrapped action to hand to the form, and a pending flag while the request is in flight. That covers the saving state and the server message with no fetch call of your own.

Two details change once you do this. The action signature gains a first parameter for the previous state, so it receives prevState before formData. The file holding the form also needs the client directive, because hooks only run in Client Components.

The action itself stays where it is. It is still exported from a server file, still receives the same FormData, and still runs only on the server, so nothing about the validation above has to change.

See displaying field-level validation errors from a Server Action for per-field errors.

Rune AI

Rune AI

Key Insights

  • Put use server at the top of the file that exports the action.
  • Point the form action prop at the exported async function.
  • Read the submitted fields from the FormData argument.
  • Validate inside the action, not just in the form.
  • Turn the form into a Client Component to display the returned result.
RunePowered by Rune AI

Frequently Asked Questions

Does a Server Action form work without JavaScript?

Yes. A form with an action prop is progressive enhanced, so the browser can submit it even before JavaScript loads or when JavaScript is disabled.

Where does the database write go?

Inside the Server Action, after validation passes. The action runs only on the server, so that is where you call your database client or data access layer.

Do I need a route handler to submit a form?

No. The Server Action is the endpoint. You can use a route handler instead when you need a public API or a non-form client, but a form does not require one.

Conclusion

A Server Action form is a plain form element whose action prop points at an async server function. The browser sends FormData, the action validates and mutates on the server, and progressive enhancement keeps the form working before JavaScript loads.