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:

bash
npm install @react-email/components
tsx
// 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:

typescript
// 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 prevState es necesario cuando usas useActionState en el componente (el hook pasa el estado anterior como primer argumento)
  • La propiedad react de Resend acepta componentes de React Email directamente -- no necesitas renderizar a HTML manualmente
  • replyTo se 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:

tsx
// 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 al action del formulario
  • isPending -- un booleano que es true mientras 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:

typescript
'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:

typescript
'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 }
}