Ilustración de Server Actions en Astro
Ilustración de Server Actions en Astro

Server Actions en Astro: Formularios Bien Hechos

¿Porque Server Actions?

Antes de los server actions de Astro, manejar formularios en sitios estáticos significaba:

Los server actions cambian todo. Te dan manejo de formularios type-safe, validado y del lado del servidor con una sola definición de función. Déjame mostrarte cómo implementé un formulario de contacto con manejo de errores y seguridad adecuados.

Definiendo la Action

Los server actions viven en src/actions/. Aquí hay una action real para enviar emails:

// src/actions/sendEmail.ts
import { ActionError, defineAction } from "astro:actions";
import {
  EMAIL_SERVICE_API_KEY,
  EMAIL_SERVICE_FROM_EMAIL,
  EMAIL_SERVICE_FROM_NAME,
  EMAIL_SERVICE_TO_EMAIL,
} from "astro:env/server";
import { EMAIL_SERVICE } from "EMAIL_SERVICE";
import { z } from "astro:schema";

const EmailService = new EMAIL_SERVICE(EMAIL_SERVICE_API_KEY);

export const sendEmail = defineAction({
  accept: "form",

  input: z.object({
    name: z.string(),
    email: z.string().email(),
    message: z.string().min(10).max(500),
  }),

  handler: async (input) => {
    const { data, error } = await EmailService.emails.send({
      from: `${EMAIL_SERVICE_FROM_NAME} <${EMAIL_SERVICE_FROM_EMAIL}>`,
      to: [EMAIL_SERVICE_TO_EMAIL],
      subject: `Email de ${input.name}`,
      html: `
        <h1>Nuevo mensaje de ${input.name}</h1>
        <p><strong>Email:</strong> ${input.email}</p>
        <p><strong>Mensaje:</strong> ${input.message}</p>
      `,
    });

    if (error)
      throw new ActionError({
        code: "INTERNAL_SERVER_ERROR",
        message: "Error al enviar email",
      });

    return data;
  },
});

Puntos clave:

Variables de Entorno Type-Safe

Astro 5 introdujo astro:env/server para variables de entorno type-safe:

// astro.config.mjs
export default defineConfig({
  env: {
    schema: {
      EMAIL_SERVICE_API_KEY: envField.string({
        context: "server",
        access: "secret",
      }),
      EMAIL_SERVICE_TO_EMAIL: envField.string({
        context: "server",
        access: "public",
        default: "[email protected]",
      }),
    },
  },
});

Ahora TypeScript sabe exactamente qué variables de entorno existen y sus tipos.

Manejo de Errores con Sentido

Mapea errores de API a respuestas amigables para el usuario:

handler: async (input) => {
  try {
    const { data, error } = await EMAIL_SERVICE.emails.send({
      /* ... */
    });

    if (error) {
      // Mapear tipos de error específicos
      if (error.name === "validation_error") {
        throw new ActionError({
          code: "BAD_REQUEST",
          message: "Formato de email o contenido inválido",
        });
      }
      if (error.name === "rate_limit_exceeded") {
        throw new ActionError({
          code: "TOO_MANY_REQUESTS",
          message: "Demasiadas solicitudes. Intenta más tarde.",
        });
      }
      // Fallback por defecto
      throw new ActionError({
        code: "INTERNAL_SERVER_ERROR",
        message: "Error al enviar email. Intenta de nuevo.",
      });
    }

    return data;
  } catch (e) {
    // Re-lanzar ActionErrors, envolver otros
    if (e instanceof ActionError) throw e;
    throw new ActionError({
      code: "INTERNAL_SERVER_ERROR",
      message: "Servicio de email no disponible.",
    });
  }
};

Usando Actions en Páginas Astro

El formulario es HTML puro - no requiere JavaScript:

---
import { actions } from 'astro:actions'

export const prerender = false  // Requerido para manejo del servidor

const result = Astro.getActionResult(actions.sendEmail)
---

{result && !result.error && (
  <p class="success">¡Email enviado exitosamente!</p>
)}

{result?.error && (
  <p class="error">{result.error.message}</p>
)}

<form method="POST" action={actions.sendEmail}>
  <label for="name">Nombre</label>
  <input type="text" id="name" name="name" required />

  <label for="email">Email</label>
  <input type="email" id="email" name="email" required />

  <label for="message">Mensaje</label>
  <textarea id="message" name="message" required></textarea>

  <button type="submit">Enviar</button>
</form>

Nota:

Campos Honeypot para Protección contra Bots

Agrega un campo oculto que los bots llenarán pero los humanos no:

<form method="POST" action={actions.sendEmail}>
  <!-- Honeypot - oculto para humanos, visible para bots -->
  <input
    type="text"
    name="botcheck"
    style="position: absolute; left: -9999px;"
    tabindex="-1"
    autocomplete="off"
  />

  <!-- Campos reales -->
  <input type="text" name="name" required />
  <!-- ... -->
</form>

Luego valida en tu action:

input: z.object({
  name: z.string(),
  email: z.string().email(),
  message: z.string().min(10).max(500),
  botcheck: z.string().max(0).optional(), // Debe estar vacío
}),

handler: async (input) => {
  if (input.botcheck) {
    // Rechazo silencioso - no dejar que los bots sepan que fueron detectados
    return { success: true }
  }
  // Continuar con envío real
}

Exportando Actions

Todas las actions deben exportarse desde src/actions/index.ts:

// src/actions/index.ts
import { sendEmail } from "./sendEmail";

export const server = {
  sendEmail,
  // Agregar más actions aquí
};

Mejora Progresiva

Para UX mejorada con JavaScript, puedes enviar vía fetch:

const form = document.querySelector("form");

form.addEventListener("submit", async (e) => {
  e.preventDefault();
  const formData = new FormData(form);

  const response = await fetch(form.action, {
    method: "POST",
    body: formData,
  });

  const result = await response.json();
  // Manejar resultado...
});

Pero el formulario funciona sin JavaScript también - esa es la belleza de los server actions.

Puntos Clave

  1. Type-safe por defecto - Zod valida entrada, TypeScript valida uso
  2. Secretos seguros - Variables de entorno solo-servidor nunca se filtran al cliente
  3. Mejora progresiva - Funciona sin JS, mejorable con JS
  4. Manejo de errores - Mapear errores de API a mensajes amigables
  5. Protección contra bots - Campos honeypot son simples y efectivos

Los server actions hacen de Astro un competidor serio para aplicaciones full-stack, no solo sitios estáticos. La experiencia de desarrollador es excelente - define una vez, usa en todas partes, con type safety completo.

¡Link copiado!

Comments for srvrct