NextjsForms with Server Actions

Forms with Server Actions

The most common place to use a Server Action is a form. Instead of attaching an onSubmit handler that calls preventDefault and manually reads input values, you pass the Server Action straight to the form's action prop: <form action={myServerAction}>. The browser's native form submission mechanism does the rest.
The action receives a FormData object
When a form submits this way, Next.js calls your Server Action with a single argument: a standard Web FormData object containing every named field in the form. There's no manual parsing of a request body — extract each field with formData.get('fieldName'), where 'fieldName' matches the input's name attribute.
Worked example: a comment form

Here's a simple comment form that validates its input and saves it via a Server Action, entirely on the server.

app/actions/comments.ts

TS
'use server'

export async function addComment(formData: FormData) {
  const author = formData.get('author')
  const body = formData.get('body')

  if (typeof author !== 'string' || author.trim() === '') {
    throw new Error('Author is required')
  }
  if (typeof body !== 'string' || body.trim() === '') {
    throw new Error('Comment cannot be empty')
  }

  // ...save { author, body } to the database...
}

app/posts/[slug]/CommentForm.tsx

TSX
import { addComment } from '@/app/actions/comments'

export default function CommentForm() {
  return (
    <form action={addComment}>
      <input name="author" placeholder="Your name" required />
      <textarea name="body" placeholder="Your comment" required />
      <button type="submit">Post comment</button>
    </form>
  )
}
Notice that CommentForm has no 'use client' directive — it's a plain Server Component. Passing a Server Action to action works whether the form itself is rendered on the server or the client.
This even works with JavaScript disabled
This is a genuinely easy-to-overlook benefit: because <form action={...}> is built on the browser's native form submission behavior, the comment form above posts successfully even if the visitor has JavaScript turned off, or the bundle hasn't finished loading yet. The browser submits the form as a normal navigation, Next.js intercepts it on the server, runs the action, and re-renders the page with the result. This is progressive enhancement — the base case works without client-side JavaScript, and JavaScript (when available) upgrades the experience with things like pending states and inline error messages, covered on the next page.
Extracting multiple fields and types
FormData values are always strings (or File objects for file inputs) — even checkboxes and numbers come through as strings, so convert them explicitly.

Extracting a checkbox and a number

TS
'use server'

export async function updateSettings(formData: FormData) {
  const displayName = formData.get('displayName') as string
  const isPublic = formData.get('isPublic') === 'on'
  const age = Number(formData.get('age'))

  // ...persist the parsed values...
}
Tip
Always re-validate on the server, even if you also validate in the browser. Anyone can submit a form (or a raw request) that skips your client-side checks entirely — the Server Action is the last line of defense, so treat every field as untrusted input.
  • Pass a Server Action directly to a form: <form action={myAction}> — no onSubmit handler or preventDefault needed.

  • The action receives a FormData object as its only argument; read fields with formData.get('fieldName').

  • Every FormData value is a string (or File) — convert booleans and numbers explicitly.

  • Because it rides on native form submission, this pattern works even with JavaScript disabled — a real progressive-enhancement win.

  • Always validate fields again on the server; client-side validation can always be bypassed.