Emails con Server Actions
Las Server Actions son la forma más limpia de enviar emails desde formularios en NextJS. No necesitas crear un endpoint API separado, no necesitas manejar fetch manualmente y obtienes tipado de TypeScript de extremo a extremo.
En esta página vas a construir un formulario de contacto completo con un template de React Email, una Server Action que procesa el envío y un componente con feedback visual para el usuario.
Template del Email con React Email
Primero, crea el template del email usando React Email. Esto te permite diseñar emails con componentes de React en vez de escribir HTML crudo:
npm install @react-email/components// emails/contacto.tsx
import { Html, Head, Body, Container, Text, Hr, Preview } from '@react-email/components'
interface ContactoEmailProps {
nombre: string
email: string
mensaje: string
}
export function ContactoEmail({ nombre, email, mensaje }: ContactoEmailProps) {
return (
<Html>
<Head />
<Preview>Nuevo mensaje de {nombre}</Preview>
<Body style={{ backgroundColor: '#ffffff', fontFamily: 'sans-serif' }}>
<Container style={{ maxWidth: '600px', margin: '0 auto', padding: '20px' }}>
<Text style={{ fontSize: '20px', fontWeight: 'bold' }}>Nuevo mensaje de contacto</Text>
<Text><strong>Nombre:</strong> {nombre}</Text>
<Text><strong>Email:</strong> {email}</Text>
<Hr />
<Text>{mensaje}</Text>
</Container>
</Body>
</Html>
)
}El componente Preview define el texto que aparece en la vista previa del email en la bandeja de entrada (el snippet que ves antes de abrir el correo). Los estilos son inline porque la mayoría de clientes de email no soportan CSS externo.
Server Action
Ahora crea la Server Action que recibe los datos del formulario, valida y envía el email usando el template:
// app/actions/contacto.ts
'use server'
import { resend } from '@/lib/resend'
import { ContactoEmail } from '@/emails/contacto'
export async function enviarContacto(
prevState: { success: boolean; error?: string } | null,
formData: FormData
) {
const nombre = formData.get('nombre') as string
const email = formData.get('email') as string
const mensaje = formData.get('mensaje') as string
if (!nombre || !email || !mensaje) {
return { success: false, error: 'Todos los campos son obligatorios' }
}
const { error } = await resend.emails.send({
from: 'Contacto <contacto@midominio.com>',
to: ['tu@email.com'],
replyTo: email,
subject: `Mensaje de ${nombre}`,
react: ContactoEmail({ nombre, email, mensaje })
})
if (error) {
return { success: false, error: 'No se pudo enviar el mensaje. Intenta de nuevo.' }
}
return { success: true }
}Puntos clave de esta Server Action:
- La directiva
'use server'marca el archivo como Server Action -- NextJS sabe que estas funciones se ejecutan en el servidor - El parámetro
prevStatees necesario cuando usasuseActionStateen el componente (el hook pasa el estado anterior como primer argumento) - La propiedad
reactde Resend acepta componentes de React Email directamente -- no necesitas renderizar a HTML manualmente replyTose configura con el email del usuario para que puedas responder directo desde tu bandeja
Componente del Formulario
El formulario usa useActionState (antes conocido como useFormState) para manejar el estado de la acción y mostrar feedback al usuario:
// components/FormularioContacto.tsx
'use client'
import { useActionState } from 'react'
import { enviarContacto } from '@/app/actions/contacto'
export function FormularioContacto() {
const [state, formAction, isPending] = useActionState(enviarContacto, null)
return (
<form action={formAction} className="space-y-4 max-w-md">
<div>
<label htmlFor="nombre" className="block text-sm font-medium">Nombre</label>
<input
id="nombre"
name="nombre"
required
className="w-full border rounded px-3 py-2"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium">Email</label>
<input
id="email"
name="email"
type="email"
required
className="w-full border rounded px-3 py-2"
/>
</div>
<div>
<label htmlFor="mensaje" className="block text-sm font-medium">Mensaje</label>
<textarea
id="mensaje"
name="mensaje"
required
rows={4}
className="w-full border rounded px-3 py-2"
/>
</div>
<button
type="submit"
disabled={isPending}
className="bg-black text-white px-6 py-2 rounded disabled:opacity-50"
>
{isPending ? 'Enviando...' : 'Enviar mensaje'}
</button>
{state?.error && (
<p className="text-red-500 text-sm">{state.error}</p>
)}
{state?.success && (
<p className="text-green-500 text-sm">Mensaje enviado correctamente.</p>
)}
</form>
)
}El hook useActionState devuelve tres valores:
state-- el valor retornado por la Server Action (null al inicio, luego{ success, error })formAction-- la función que pasas alactiondel formularioisPending-- un booleano que estruemientras la acción se está ejecutando
useActionState en NextJS 15+
useActionState (antes useFormState) es el hook recomendado en NextJS 15 y React 19 para manejar Server Actions en formularios. Si vienes de versiones anteriores, el cambio es solo de nombre -- la API es la misma.
Otros Casos de Uso con Server Actions
El patrón es el mismo para cualquier tipo de email que el usuario dispare desde la interfaz:
Invitar usuarios por email:
'use server'
import { resend } from '@/lib/resend'
export async function invitarUsuario(formData: FormData) {
const email = formData.get('email') as string
const rol = formData.get('rol') as string
const { error } = await resend.emails.send({
from: 'Mi App <noreply@midominio.com>',
to: [email],
subject: 'Te invitaron a unirte al equipo',
html: `<p>Has sido invitado como <strong>${rol}</strong>.</p>
<a href="https://miapp.com/invitacion?email=${email}">Aceptar invitación</a>`
})
if (error) return { success: false }
return { success: true }
}Reenviar email de confirmación:
'use server'
import { resend } from '@/lib/resend'
export async function reenviarConfirmacion(formData: FormData) {
const email = formData.get('email') as string
const token = crypto.randomUUID()
// Guardar el token en tu base de datos...
const { error } = await resend.emails.send({
from: 'Mi App <noreply@midominio.com>',
to: [email],
subject: 'Confirma tu email',
html: `<a href="https://miapp.com/confirmar?token=${token}">Confirmar email</a>`
})
if (error) return { success: false }
return { success: true }
}