Guía Completa de Formularios con Next.js Server Actions

4 min de lectura
LevelAR Team
Desarrollador trabajando en formularios en un editor de código con React.

Una de las tareas más comunes en el desarrollo web es manejar formularios. Tradicionalmente en Next.js, esto implicaba crear rutas de API, gestionar el estado del lado del cliente con useState, y usar fetch para enviar los datos. Con la llegada de las Server Actions, este proceso se ha simplificado drásticamente, permitiéndonos escribir código más limpio y seguro.

Las Server Actions permiten que tus componentes de React ejecuten funciones directamente en el servidor, eliminando la necesidad de crear endpoints de API intermedios. Veamos cómo construir un formulario de contacto robusto desde cero.

Paso 1: Definir el Esquema de Validación con Zod

La validación nunca debe confiarse solo al cliente. Zod es la herramienta perfecta para asegurar la integridad de los datos en el servidor.

Primero, instalá Zod:

npm install zod

Luego, creá un esquema para tu formulario en un archivo como lib/schemas.ts:

// lib/schemas.ts
import { z } from 'zod';

export const contactSchema = z.object({
  name: z.string().min(3, 'El nombre es demasiado corto'),
  email: z.string().email('El email no es válido'),
  message: z.string().min(10, 'El mensaje debe tener al menos 10 caracteres'),
});

Paso 2: Crear la Server Action

La Server Action es una función asíncrona que se ejecuta en el servidor. Aquí es donde recibimos los datos, los validamos con Zod y realizamos una acción (como guardar en una base de datos).

Creá un archivo app/actions.ts:

// app/actions.ts
'use server';

import { z } from 'zod';
import { contactSchema } from '@/lib/schemas';
import { revalidatePath } from 'next/cache';

export type FormState = {
  message: string;
  errors?: Record<string, string[] | undefined>;
  success: boolean;
};

export async function createContactRequest(prevState: FormState, formData: FormData): Promise<FormState> {
  const rawData = Object.fromEntries(formData.entries());

  const validatedFields = contactSchema.safeParse(rawData);

  if (!validatedFields.success) {
    return {
      message: 'Error de validación. Por favor, revisa los campos.',
      errors: validatedFields.error.flatten().fieldErrors,
      success: false,
    };
  }

  // Simulación de guardado en base de datos
  console.log('Datos recibidos:', validatedFields.data);
  await new Promise(resolve => setTimeout(resolve, 1000));

  // Revalida el caché de la página para mostrar nuevos datos si es necesario
  revalidatePath('/');

  return { message: '¡Gracias por tu mensaje! Te contactaremos pronto.', success: true };
}

Paso 3: Construir el Componente del Formulario

Ahora, creamos el componente de React que usará esta acción. Usaremos el hook useFormState para manejar el estado del formulario (errores, mensajes de éxito).

// app/contact-form.tsx
'use client';

import { useFormState } from 'react-dom';
import { createContactRequest, FormState } from '@/app/actions';
import { SubmitButton } from './submit-button';

const initialState: FormState = { message: '', success: false };

export function ContactForm() {
  const [state, formAction] = useFormState(createContactRequest, initialState);

  return (
    <form action={formAction} className="space-y-4">
      <div>
        <label htmlFor="name">Nombre</label>
        <input type="text" id="name" name="name" required />
        {state.errors?.name && <p className="text-red-500">{state.errors.name[0]}</p>}
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <input type="email" id="email" name="email" required />
        {state.errors?.email && <p className="text-red-500">{state.errors.email[0]}</p>}
      </div>
      <div>
        <label htmlFor="message">Mensaje</label>
        <textarea id="message" name="message" required />
        {state.errors?.message && <p className="text-red-500">{state.errors.message[0]}</p>}
      </div>
      
      <SubmitButton />

      {state.message && (
        <p className={state.success ? 'text-green-500' : 'text-red-500'}>
          {state.message}
        </p>
      )}
    </form>
  );
}

Paso 4: Feedback de UI con useFormStatus

Para una mejor UX, es crucial deshabilitar el botón de envío y mostrar un estado de carga. Creamos un componente separado que usa el hook useFormStatus.

// app/submit-button.tsx
'use client';

import { useFormStatus } from 'react-dom';

export function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Enviando...' : 'Enviar'}
    </button>
  );
}

Cuándo NO usar Server Actions

Aunque son increíblemente útiles, no son la solución para todo:

  • Endpoints Públicos Reutilizables: Si necesitás una API para ser consumida por terceros (otra app, un servicio externo), una ruta de API tradicional (/api/...) sigue siendo la mejor opción.
  • Flujos Complejos o en Tiempo Real: Para WebSockets, streaming de video o colas de trabajo pesadas, las Server Actions no son el mecanismo adecuado. Necesitarás un servidor dedicado o servicios especializados.
  • Autenticación Basada en Tokens para SPAs: Si tu frontend es una SPA totalmente separada que se comunica con Next.js solo como un backend, probablemente seguirás un flujo de autenticación basado en tokens a través de rutas de API.

Conclusión

Las Server Actions, combinadas con hooks como useFormState y useFormStatus, representan un gran avance en el desarrollo con Next.js. Simplifican la arquitectura, mejoran la seguridad al centralizar la lógica en el servidor y ofrecen una experiencia de desarrollo más fluida. Para la gran mayoría de los formularios en una aplicación web, este es el nuevo estándar a seguir.

nextjs server-actions formularios zod react

Compartir este artículo:

¿Querés más contenido como este?

Recibí actualizaciones curadas sobre desarrollo web moderno, casos reales y oportunidades para acelerar tu producto digital.