Creating Forms with Server Actions in Next.js – Validation, Secure Submission, and Optimistic UI Updates

Server Actions in Next.js allow you to handle form submissions directly on the server without needing separate API routes. This article walks through how to define forms, extract FormData, validate inputs with zod, manage submission states, and implement optimistic UI updates.

Server ActionsuseActionStateForm submissionValidation

~3 min read • Updated Oct 27, 2025

Introduction


In Next.js, you can submit forms using Server Actions, which execute directly on the server. This eliminates the need for separate API routes and simplifies data handling.


How It Works


Use the action attribute on a form to invoke a Server Function. The function automatically receives a FormData object:

export default function Page() {
  async function createInvoice(formData: FormData) {
    'use server'
    const rawFormData = {
      customerId: formData.get('customerId'),
      amount: formData.get('amount'),
      status: formData.get('status'),
    }
    // mutate data
  }

  return <form action={createInvoice}>...</form>
}

Passing Additional Arguments


You can pass extra arguments using bind:

'use client'
const updateUserWithId = updateUser.bind(null, userId)

<form action={updateUserWithId}>
  <input name="name" />
</form>

On the server:

'use server'
export async function updateUser(userId: string, formData: FormData) {}

Form Validation


Use libraries like zod for server-side validation:

'use server'
import { z } from 'zod'

const schema = z.object({
  email: z.string().email(),
})

export async function createUser(formData: FormData) {
  const validated = schema.safeParse({
    email: formData.get('email'),
  })

  if (!validated.success) {
    return {
      errors: validated.error.flatten().fieldErrors,
    }
  }

  // mutate data
}

Displaying Validation Errors with useActionState


Use useActionState in a Client Component to manage form state:

'use client'
import { useActionState } from 'react'

const initialState = { message: '' }

export function Signup() {
  const [state, formAction, pending] = useActionState(createUser, initialState)

  return (
    <form action={formAction}>
      <input name="email" required />
      <p>{state?.message}</p>
      <button disabled={pending}>Sign up</button>
    </form>
  )
}

Managing Submission State


Use useFormStatus to show loading indicators:

'use client'
import { useFormStatus } from 'react-dom'

export function SubmitButton() {
  const { pending } = useFormStatus()
  return <button disabled={pending}>Sign Up</button>
}

Optimistic UI Updates with useOptimistic


Update the UI before the server responds:

'use client'
import { useOptimistic } from 'react'

const [optimisticMessages, addMessage] = useOptimistic(messages, (state, msg) => [...state, { message: msg }])

const formAction = async (formData: FormData) => {
  const msg = formData.get('message')
  addMessage(msg)
  await send(msg)
}

Programmatic Form Submission


Use requestSubmit() to trigger form submission manually:

'use client'
const handleKeyDown = (e) => {
  if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
    e.preventDefault()
    e.currentTarget.form?.requestSubmit()
  }
}

Conclusion


Server Actions in Next.js offer a secure and elegant way to handle form submissions. With built-in support for validation, state management, and optimistic updates, you can build interactive and reliable forms with ease.


Written & researched by Dr. Shahin Siami