📨 Formularios de contacto en Astro con Server Actions y Resend

Introducción

Tener un formulario de contacto es algo casi estándar en cualquier web corporativa o sitio personal.

¿Y qué implica esto normalmente? Que tenemos un formulario en el frontend que, al enviarse, realiza una petición a un backend (propio o de terceros) encargado de procesar los datos y enviar un correo electrónico a una cuenta de contacto.

Hasta aquí, todo bien. Pero si trabajamos con Astro en modo SSG (Static Site Generation), las cosas se complican un poco.

En un proyecto Astro SSG todo lo que tenemos es HTML y JS, servidos al frontend. Y hoy en día, para enviar un correo, necesitamos un API token o credenciales que deben mantenerse en un entorno seguro (del lado del servidor), para evitar que alguien pueda robarlas y enviar correos en nuestro nombre.

Si intentaremos enviar un correo desde el cliente, expondríamos el api token y un usuario malicioso podría aprovecharlo

La opción habitual sería montar un backend propio (con Express, Fastify, Laravel, etc.) o depender de servicios externos (como Formspree, Netlify Forms o Zapier).

Con un backend propio el token api está seguro en el backend, no viaja al navegador

Esto funciona, pero tiene varios inconvenientes:

  • Estamos usando una infraestructura sobredimensionada para algo tan simple como un formulario.
  • Hay que configurar y desplegar un servidor o integrar el frontend dentro del backend.
  • Implica lidiar con CORS, rutas, seguridad y autenticación.
  • Añadimos mantenimiento y complejidad al proyecto.
  • O quedamos atados a un proveedor con sus propias limitaciones.

Entonces, ¿no hay una forma más sencilla?
Sí: Astro Server Actions.

Esta funcionalidad nos permite:

  • Mantener nuestro proyecto Astro SSG.
  • Definir acciones del lado del servidor dentro del propio proyecto.
  • Manejar formularios sin necesidad de montar un backend completo.

Con Server Actions un atacante no puede acceder al API token ya que se almacena en el lado del servidor

Cómo funciona

Astro ofrece server adapters que permiten configurar un mini servidor o funciones server less dentro del mismo proyecto. Entre los oficiales tenemos:

  • Node.js: servidor HTTP continuo, ideal para VPS, App Service o Docker.
  • Vercel: funciones serverless (cada request ejecuta el código en una función aislada).
  • Netlify: funciones serverless similares.
  • Cloudflare: workers o pages functions (sin Node, muy rápido y distribuido en el edge).

Cuando desplegamos el proyecto, este mini servidor / funciones procesa las acciones que definimos en el propio código de Astro.

En resumen: tenemos un microservidor o funciones serverless integradas en el mismo proyecto Astro. Ya no basta con un hosting estático, sino que necesitamos un entorno que soporte Node.js o el adapter que hayamos elegido.

Para enviar correos, podemos usar un servicio especializado como Resend, que ofrece una API moderna, segura y fácil de integrar.

Manos a la obra

Vamos a montar un ejemplo completo: un formulario de contacto funcional con:

Al final tendrás un formulario operativo, sin frameworks adicionales ni configuraciones complejas.


⚙️ 1. Configuración inicial

Hora de arrancar Visual Studio Code (o tu editor favorito) y vamos a ello.

Abrimos el terminal y creamos un nuevo proyecto Astro:

npm create astro@latest my-contact-form

Si ya tienes la carpeta creada y vacía, puedes usar npm create astro@latest . para inicializarlo ahí mismo.

Elegimos la opción use minimal (empy) template.

Elegir minimal template

Luego, indícale que instale las dependencias.

hace un npm install por nosotros

Si se te olvida este paso, después puedes hacer un npm install manualmente.

E iniciamos un repositorio Git (esto es opcional).

inicializar Git

Navega al directorio del proyecto e inicia el proyecto:

cd my-contact-form
npm run dev

También puedes hacer un open folder desde Visual Studio Code, y ejecutar directamente el comando npm run dev.

⚙️ 2. Configura Server Actions

Ya tenemos nuestro proyecto en blanco creado y funcionando, ahora vamos a configurar las Server Actions, para poder definir acciones del lado del servidor.

En nuestro caso elegimos instalar el adaptador de Node.js, así evitamos atarnos a un proveedor de cloud en concreto (puedes elegir otro según tu plataforma de despliegue). Y ejecutamos el siguiente comando desde el terminal:

npm install @astrojs/node

Ahora toca indicarle a nuestro proyecto Astro que use este adaptador, para ello tenemos que actualizar el fichero de configuración astro.config.mjs:

// @ts-check
import { defineConfig} from 'astro/config';
+ import node from '@astrojs/node';

// https://astro.build/config
export default defineConfig({
+  adapter: node({
+    mode: 'standalone',
+  }),
});

El modo standalone crea un servidor ejecutable. Si prefieres integrarlo en un servidor Node.js existente, usa el modo middleware.

✉️ 3. Instala Resend

Para el envío de correos existen múltiples opciones y plataformas, por ejemplo: Sendgrid, Postmark, Mailgun, Amazon SES, etc.

En este ejemplo usaremos Resend, una alternativa moderna, con una API sencilla y un plan gratuito de hasta 1.000 correos al día.

Un resumen de los pasos que vamos a seguir:

  • En caso de que no lo tengas ya, crea una cuenta y obtenen tu API key para poder enviar correos (para el ejemplo, nos vale así, si quisieramos enviar desde nuestro dominio, tendríamos que verificarlo, etc...).

  • Instalar el SDK oficial de Resend.

  • Añadir las variables de entorno necesarias.

  • Crear el formulario en Astro.

  • Crear una server action que será la encargada de enviar el correo.

  • Conectar el post o un evento de submit del formulario con la acción.

Manos a la obra, nos vamos a la terminal e instalamos el SDK oficial:

npm install resend

Ahora vamos a crear un fichero .env en la raíz del proyecto para guardar las variables de entorno necesarias, en este caso:

  • La API key de Resend de tu cuenta (si te creas una cuenta gratuita, puedes usar la que te proporcionan).

  • El correo desde el que se enviarán los mensajes (debe estar registrado como Dominio en Resend, para una prueba rápida, puedes usar onboarding@resend.dev).

  • El correo al que se enviarán los mensajes (puedes poner un correo tuyo para hacer pruebas, en caso de que no te llegue el correo, chequea tu carpeta de spam).

.env

RESEND_API_KEY=tu_api_key_aqui
FROM_EMAIL=tu_email_verificado_aqui
TO_EMAIL=email_destino_aqui

En este ejemplo, FROM_EMAIL es el correo desde el que se enviarán los mensajes (debe estar registrado como Dominio en Resend). Para hacer una prueba puedes usar el correo onboarding@resend.dev.

La API Key te la provee Resend al crear tu cuenta, pero cuidado, la tienes que copiar a tu local y no podrás volver a verla, si la pierdes, tendrás que generar una nueva, en el caso de que necesitas generar una nueva el proceso es sencillo, solo tienes que ir a la sección de API Keys en tu cuenta de Resend y crear una nueva.

Como obtener una API Key nueva en resend

Y para poder usar estas variables de entorno en nuestro proyecto Astro, y tenerlas tipadas, así como organizadas (distingue entre cliente y servidor). Vamos añadir la configuración en el fichero astro.config.mjs:

- import { defineConfig } from 'astro/config';
+ import { defineConfig, envField } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  adapter: node({
    mode: "standalone",
  }),
+  env: {
+    schema: {
+      RESEND_API_KEY: envField.string({
+        context: 'server',
+        access: 'secret',
+      }),
+      FROM_EMAIL: envField.string({
+        context: 'server',
+        access: 'secret',
+      }),
+      TO_EMAIL: envField.string({
+        context: 'server',
+        access: 'secret',
+      }),
+    },
+  },
});

Aquí le indicamos que estas variables de entorno son de servidor (context: 'server'), y que son secretas. Para poder importar esas variables desde un fichero .astro, nos hará falta primero compilar el proyecto para que se generen los tipos.

Hacemos un build para que se generen los tipos para estas variables de entorno:

npm run build

📝 4. Crea el formulario en Astro

Creamos un formulario sencillo en src/pages/index.astro con los campos nombre, correo y mensaje.

Formulario sencillo de contacto

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width" />
    <meta name="generator" content={Astro.generator} />
    <title>Astro</title>
  </head>
  <body>
    <h1>Astro</h1>
    <h1>Astro</h1>
+    <form method="POST">
+      <input name="name" type="text" placeholder="Name" required />
+      <input name="email" type="email" placeholder="Email" required />
+      <textarea name="message" placeholder="Message"></textarea>
+      <button type="submit"> Send </button>
+    </form>
  </body>
</html>

+<style>
+  body {
+    font-family: system-ui, sans-serif;
+  }
+  form {
+   margin-left: auto;
+    margin-right: auto;
+    display: flex;
+    flex-direction: column;
+    gap: 20px;
+    max-width: 500px;
+    margin-top: 20px;

+    input {
+      padding: 10px;
+      font-size: 16px;
+      border: 1px solid #ccc;
+      border-radius: 4px;
+    }

+    textarea {
+      font-family: inherit;
+      padding: 10px;
+      font-size: 16px;
+      border: 1px solid #ccc;
+      border-radius: 4px;
+      min-height: 100px;
+    }

+    button {
+      padding: 10px;
+      font-size: 16px;
+      background-color: #007bff;
+      color: white;
+      border: none;
+      border-radius: 4px;
+      cursor: pointer;
+    }

+    button:hover {
+      background-color: #0056b3;
+    }
+  }
+</style>

Es un formulario simple con estilos básicos. Añadiremos la funcionalidad más adelante.

🛠️ 5. Crear la acción del servidor

Volvemos a la parte de backend, es hora de definir la acción que se ejecutará cuando se envíe el formulario.

En este caso, Astro funciona con convención sobre configuración, por lo que las acciones del servidor se definen en archivos dentro de la carpeta src/actions y debajo de esa carpeta creamos un fichero index.ts para definir nuestra acción que enviará el correo (podemos definir más de una acción en ese fichero):

src/actions/index.ts

import { Resend } from "resend";
import { defineAction } from "astro:actions";
import { RESEND_API_KEY, FROM_EMAIL, TO_EMAIL } from "astro:env/server";
import { z } from "astro:schema";

const resend = new Resend(RESEND_API_KEY);

export const server = {
  sendMail: defineAction({
    accept: "form",
    input: z.object({
      name: z.string().min(1, "Name is required"),
      email: z.string().email("Invalid email"),
      message: z.string().optional(),
    }),
    handler: async (input) => {
      try {
        const { name, email, message = "" } = input;

        await resend.emails.send({
          from: FROM_EMAIL,
          to: TO_EMAIL,
          subject: `Nuevo mensaje de contacto de ${name}`,
          html: `
            <h2>Nuevo mensaje de contacto</h2>
            <p><strong>Nombre:</strong> ${name}</p>
            <p><strong>Email:</strong> ${email}</p>
            <h3>Mensaje:</h3>
            <p>${message.replace(/\n/g, "<br>")}</p>
          `,
        });

        return { success: true, message: "E-mail sent successfully ✅" };
      } catch (error) {
        return {
          success: false,
          message: "There was an error sending the e-mail ❌",
        };
      }
    },
  }),
};

¿Qué estamos haciendo aquí?

  • Instanciamos el cliente de Resend con la API key que hemos definido en las variables de entorno. Un tema interesante, fíjate en el import de las variables de entorno, usamos astro:env/server, si intentáramos importar esto dentro de un script que se ejecute en el cliente, nos daría error (en la consola del navegador aparecería net::ERR_ABORTED 500 (Internal Server Error) al intentar hacer el import desde cliente), esto es de gran ayuda para evitar errores de seguridad.

  • Definimos un objeto server que contiene las acciones que queremos exponer y los exportamos.

  • En este caso, el objeto server tiene una acción llamada sendMail.

  • En esa accíon le indicamos:

    • Que vamos a leer datos de un formulario.

    • Añadimos una validación básica con zod (que ya viene integrado en Astro) en la que indicamos que el nombre es obligatorio, el correo debe ser válido y el mensaje es opcional.

    • En el manejador de la acción (handler), extraemos los datos del formulario y usamos el cliente de Resend para enviar el correo.

Un tema interesante: debemos de volver a compilar el proyecto (o si ya hemos hecho un npm run dev al estar en modo watch lo hará por nosotros al guardar los cambios), porque de nuevo Astro va a generar el tipado de este fichero, y así será más cómodo de consumir.

6. Conectar el formulario con la acción

Ahora, volvemos a src/pages/index.astro y conectamos el formulario con la acción que acabamos de crear:

  • Le damos un Id al formulario para poder referenciarlo desde JavaScript.
  • Le indicamos que en el method es POST.
  • Añadimos un script que capture el evento submit del formulario, y aquí lo que hacemos es:
    • Prevenir el comportamiento por defecto del formulario (que no haga un post completo de página al hacer un submit).
    • Recopilamos los datos del formulario.
    • Invocamos a la server action que hemos definido anteriormente para que haga el envío de correo.
    • Añadimos un await y esperamos el resultado de dicha llamada a servidor, en caso de que tenga éxito, borramos el contenido del formulario (también podríamos haber puesto un mensaje de éxito).

-     <form method="POST" class="grid gap-4">
+     <form id="contact-form" method="POST" class="grid gap-4">
        ... Mismo contenido del formulario ...
      </form>
    </body>
  </html>
+ <script>
+  import { actions } from 'astro:actions';
+  const form = document.getElementById('contact-form');
+  export const handleSubmit = async (event: Event) => {
+    event.preventDefault();
+    const form = event.target as HTMLFormElement;
+    const sendFormData = new FormData(form);
+    const result = await actions.sendMail(sendFormData);
+    if (result.data?.success) {
+      form.reset();
+    }
+  };
+  if (form) {
+    form.addEventListener('submit', handleSubmit);
+  }
+ </script>

<style>

Este es un ejemplo básico, si quieres seguir profundizando, podrías mejorar la experiencia de usuario mostrando mensajes de éxito o error, o validando el formulario con JavaScript.

Hemos optado por manejar el envío del formulario mediante JavaScript, ya que normalmente este formulario se encuentra dentro de un componente, y así resulta más sencillo gestionar toda la lógica desde un mismo lugar. De esta forma, además, mantenemos el sitio completamente estático.

¿Te animas a probarlo?

npm run dev

Alternativa HTML puro

Si quisiéramos, podríamos hacerlo directamente en el HTML y ejecutar acciones según el resultado de la Server Action. Sin embargo, en ese caso, toda la página se procesaría en el servidor. Además, para poder redirigir a otra página usando Astro.redirect(), la acción debe manejarse desde una página de Astro. Puedes ver la diferencia si haces una build después de cada ejemplo.

Un ejemplo de cómo enviar el formulario sin tirar de JavaScript, y usando la acción directamente en el HTML, sería el siguiente (ojo que esta página dejaría de ser estática, pasaría a ser SSR):

index.astro

---
import { actions } from 'astro:actions';
import { ClientRouter } from 'astro:transitions';

export const prerender = false; // Necesario para usar las acciones del servidor en el HTML

const result = Astro.getActionResult(actions.sendMail);
if (result?.data?.success) {
  return Astro.redirect('/');
} else if (result?.error) {
  console.error('Error sending email:', result.error);
}
---

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <meta name="viewport" content="width=device-width" />
    <meta name="generator" content={Astro.generator} />
    <title>Astro</title>
    <ClientRouter />
  </head>
  <body>
    <h1>Astro</h1>

    <form id="contact-form" method="POST" action={actions.sendMail}>
      <input name="name" type="text" placeholder="Nombre" required />
      <input name="email" type="email" placeholder="Correo" required />
      <textarea name="message" placeholder="Mensaje"></textarea>
      <button type="submit"> Enviar </button>
    </form>
  </body>
</html>

<style>
  form {
    margin-left: auto;
    margin-right: auto;
    display: flex;
    flex-direction: column;
    gap: 20px;
    max-width: 500px;
    margin-top: 20px;

    input {
      padding: 10px;
      font-size: 16px;
      border: 1px solid #ccc;
      border-radius: 4px;
    }

    textarea {
      padding: 10px;
      font-size: 16px;
      border: 1px solid #ccc;
      border-radius: 4px;
      min-height: 100px;
    }

    button {
      padding: 10px;
      font-size: 16px;
      background-color: #007bff;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }

    button:hover {
      background-color: #0056b3;
    }
  }
</style>

🚀 Conclusiones

Las Server Actions de Astro son ideales cuando queremos añadir a un proyecto SSG o SSR una pequeña capa de lógica del lado del servidor, sin montar un backend completo.

En resumen:

  • Ganas flexibilidad.
  • Mantienes tu proyecto ligero.
  • No comprometes la seguridad de tus claves.

Puedes consultar el código completo del ejemplo en GitHub:
👉 Repositorio de ejemplo

Antes de ejecutarlo, recuerda configurar tu archivo .env en la raíz del proyecto.

Publicado por el equipo de Content Island

Content Island — un headless CMS para crear, gestionar y conectar tu contenido.