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.