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

Related Articles

Advanced Client-Side Routing and Performance Hooks in Next.js

Next.js provides a rich set of client-side hooks and caching utilities that empower developers to build dynamic, responsive, and secure applications. From reading route parameters to tracking navigation state and reporting performance metrics, this guide walks you through the most important tools available in the App Router.

Continue

Handling Authorization and Caching in Next.js: A Developer’s Guide

Next.js introduces powerful experimental features for access control and smart caching. This guide covers the unauthorized() function for custom 401 handling, unstable_cache for persistent memoization, updateTag for instant cache invalidation, and useLinkStatus for inline navigation feedback. Learn how to use these tools to build secure, performant, and responsive applications.

Continue

redirect and refresh in Next.js — Smart Redirects and Client Refreshing via Server Actions

The redirect function in Next.js allows you to navigate users to a new route, returning either a 307 or 303 HTTP response depending on context. It works in Server Components, Client Components, Route Handlers, and Server Actions. The refresh function is used exclusively within Server Actions to refresh the client router. This article explains how both functions work, with practical examples and key considerations.

Continue

NextRequest and NextResponse in Next.js — Managing Cookies, Headers, Redirects, and Rewrites

Next.js extends the native Web Request and Response APIs with NextRequest and NextResponse, offering powerful tools for managing cookies, headers, redirects, rewrites, and JSON responses. These utilities simplify server-side logic and improve control over routing, personalization, and security. This guide walks through their capabilities with practical examples and best practices.

Continue

headers, ImageResponse, notFound, and permanentRedirect in Next.js — Request Handling, Dynamic Images, Errors, and Redirects

Next.js offers powerful tools for handling HTTP requests and responses in Server Components. The headers function lets you read incoming request headers. ImageResponse allows you to generate dynamic images using JSX and CSS. The notFound function renders a custom 404 page, and permanentRedirect enables permanent redirection to another route. This article explains how to use each feature with practical examples.

Continue

A Complete Guide to Using metadata and generateMetadata in Next.js

In modern versions of Next.js, managing page metadata is more powerful and intuitive than ever. Metadata is automatically injected into the <head> of your pages and plays a vital role in SEO, social sharing, and user experience. This guide explains the two main ways to define metadata: using the static metadata object and the dynamic generateMetadata function.

Continue