---
title: "Protección contra bots en Astro con Cloudflare Turnstile"
description: "Sustituya CAPTCHA por detección invisible de bots en Astro usando Cloudflare Turnstile. Proteja formularios de registro, páginas de inicio de sesión y endpoints de API contra bots sin frustrar a usuarios reales."
source: "https://www.launchfa.st/es/blog/astro-turnstile-bot-protection"
author: "Rishi Raj Jain"
created_at: 2025-12-09T12:00:00.000Z
keywords: "astro, cloudflare turnstile, bot protection, captcha alternative, form security, spam prevention"
---

> **Site index for agents**
> Fetch https://www.launchfa.st/llms.txt to discover every page on this site.
> Every page is also available as Markdown by appending `.md` to its URL.

<img width="1600" height="900" alt="Protección contra bots en Astro con Cloudflare Turnstile" decoding="async" loading="eager" class="mt-4 border rounded-sm bg-cover bg-center bg-no-repeat transform will-change-auto" src="https://ik.imagekit.io/vjeqenuhn/launchfast-website/astro-turnstile-bot-protection.png" />

Los CAPTCHA tradicionales protegen los formularios contra bots, pero también frustran a usuarios legítimos con acertijos, selección de imágenes y una accesibilidad deficiente. Google reCAPTCHA v2 es famoso por hacer que los usuarios hagan clic en semáforos hasta que abandonan la página por frustración. Incluso los CAPTCHA «invisibles» suelen fallar en silencio o degradar la experiencia de usuario.

[Cloudflare Turnstile](https://developers.cloudflare.com/turnstile/) sustituye los CAPTCHA por una detección inteligente de bots que prioriza la privacidad. Se ejecuta en segundo plano, analizando el comportamiento del navegador y las huellas del dispositivo sin pedir a los usuarios que resuelvan acertijos. La mayoría de los usuarios legítimos nunca ven un desafío. Cuando aparece, es una casilla de verificación sencilla (no una galería de imágenes de hidrantes).

En esta guía, integrará Turnstile en una aplicación Astro para proteger formularios de registro, formularios de contacto y endpoints de API. Añadirá el widget a sus formularios, verificará los tokens en el servidor y gestionará casos límite como la limitación de tasa y los estados de error. Al final, tendrá una protección contra bots lista para producción que escala globalmente en la red de Cloudflare.

- [Ver proyecto de ejemplo en GitHub](https://github.com/rishi-raj-jain/my-astro-turnstile-app)

## Requisitos previos

Necesitará lo siguiente:

- [Node.js 20](https://nodejs.org/en/blog/announcements/v20-release-announce) o posterior
- Una cuenta de [Cloudflare](https://dash.cloudflare.com/sign-up)

## Descripción general de la arquitectura

```
1. Frontend (componente Astro con widget Turnstile)
   → Renderiza el widget Turnstile en el formulario
   → Captura el token cuando el usuario completa el desafío

2. Backend (ruta de API o manejador de formulario)
   → Recibe los datos del formulario + token Turnstile
   → Verifica el token con la API de Cloudflare
   → Procesa la solicitud solo si la verificación tiene éxito

3. API de Cloudflare Turnstile
   → Valida el token
   → Devuelve éxito/fallo + metadatos
```

## Obtener las claves de API de Turnstile

Antes de construir, necesita las claves de API de Turnstile:

1. Vaya al [panel de Cloudflare](https://dash.cloudflare.com/)
2. Navegue a **Application security > Turnstile** en la barra lateral
3. Haga clic en **Add site**
4. Configure su sitio:
   - **Site name**: `my-astro-turnstile-app`
   - **Domain**: Añada su dominio (por ejemplo, **workers.dev**)
   - **Widget Mode**: Elija **Managed** (recomendado para la mayoría de casos de uso)
5. Haga clic en **Create**

Recibirá:

- **Site Key** (pública, se usa en el frontend)
- **Secret Key** (privada, se usa en el backend)

Guarde estas claves; las necesitará en los siguientes pasos.

## Crear una nueva aplicación Astro

Comencemos creando un nuevo proyecto Astro. Ejecute el siguiente comando:

```bash
npm create astro@latest my-astro-turnstile-app
cd my-astro-turnstile-app
npm install wrangler
```

Cuando se le solicite, elija:

- `Use minimal (empty) template` cuando se le pregunte cómo iniciar el nuevo proyecto.
- `Yes` cuando se le pregunte si desea instalar dependencias.
- `Yes` cuando se le pregunte si desea inicializar un repositorio git.

Una vez hecho esto, puede moverse al directorio del proyecto e iniciar la aplicación:

```bash
npm run dev
```

La aplicación debería estar ejecutándose en [localhost:4321](http://localhost:4321/).

## Integrar el adaptador de Cloudflare en su proyecto Astro

Para desplegar su proyecto Astro en Cloudflare Workers y usar variables de entorno de forma segura, necesita instalar el adaptador de Cloudflare. Ejecute el comando siguiente:

```bash
npx astro add cloudflare --yes
```

Actualice `astro.config.mjs` para que **output** sea **server**:

```javascript
// File: astro.config.mjs
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';

export default defineConfig({
  output: 'server',
  adapter: cloudflare()
});
```

## Configurar variables de entorno

Añada sus claves de Turnstile a `wrangler.jsonc`:

```jsonc
// File: wrangler.jsonc
{
	"main": "dist/_worker.js/index.js",
	"name": "my-astro-turnstile-app",
	"compatibility_date": "2025-12-08",
	"compatibility_flags": [
		"nodejs_compat",
		"global_fetch_strictly_public"
	],
	"assets": {
		"binding": "ASSETS",
		"directory": "./dist"
	},
	"observability": {
		"enabled": true
	},
    "vars": {
        "TURNSTILE_SITE_KEY": "...",
        "TURNSTILE_SECRET_KEY": "..."
    }
}
```

> **Importante**: Nunca suba su clave secreta al control de versiones. Para el desarrollo local, cree un archivo `.dev.vars`:

```bash
# File: .dev.vars
TURNSTILE_SITE_KEY="your-site-key"
TURNSTILE_SECRET_KEY="your-secret-key"
```

Para producción, configure el secreto mediante el panel de Cloudflare o la CLI:

```bash
npx wrangler secret put TURNSTILE_SITE_KEY
npx wrangler secret put TURNSTILE_SECRET_KEY
```

## Añadir tipos de TypeScript

Cree `src/env.d.ts` para añadir los tipos adecuados para las variables de entorno:

```typescript
// File: src/env.d.ts
/// <reference types="astro/client" />

type ENV = {
    TURNSTILE_SITE_KEY: string
    TURNSTILE_SECRET_KEY: string
}

type Runtime = import('@astrojs/cloudflare').Runtime<ENV>

declare namespace App {
    interface Locals extends Runtime { }
}
```

Esto le proporciona seguridad de tipos completa al acceder a `locals.runtime.env.TURNSTILE_SITE_KEY` y `locals.runtime.env.TURNSTILE_SECRET_KEY` en sus rutas.

## Crear una utilidad de verificación

Para reutilización, extraiga la lógica de verificación de Turnstile en una función de utilidad. Cree `src/lib/turnstile.ts`:

```typescript
// File: src/lib/turnstile.ts

interface TurnstileResponse {
  success: boolean
  challenge_ts?: string
  hostname?: string
  'error-codes'?: string[]
  action?: string
  cdata?: string
}

/**
 * Verify a Turnstile token with Cloudflare's API
 */
export async function verifyTurnstileToken(
  token: string,
  secretKey: string,
  remoteIP?: string
): Promise<TurnstileResponse> {
  const response = await fetch(
    'https://challenges.cloudflare.com/turnstile/v0/siteverify',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        secret: secretKey,
        response: token,
        remoteip: remoteIP, // Optional: pass client IP for additional validation
      }),
    }
  )

  if (!response.ok) {
    throw new Error(`Turnstile API error: ${response.status}`)
  }

  return response.json()
}

/**
 * Extract Turnstile token from form data or request body
 */
export function getTurnstileToken(formData: FormData): string | null {
  return formData.get('cf-turnstile-response') as string | null
}

/**
 * Helper to check if Turnstile verification is required
 */
export function isTurnstileEnabled(env: any): boolean {
  return !!(env.TURNSTILE_SITE_KEY && env.TURNSTILE_SECRET_KEY)
}
```

Ahora puede usar esta utilidad en cualquier manejador de formulario o ruta de API:

```typescript
import { verifyTurnstileToken, getTurnstileToken } from '../lib/turnstile'

const token = getTurnstileToken(formData)
if (!token) {
  return new Response('Missing verification token', { status: 400 })
}

const result = await verifyTurnstileToken(
  token,
  Astro.locals.runtime.env.TURNSTILE_SECRET_KEY
)

if (!result.success) {
  return new Response('Verification failed', { status: 403 })
}

// Proceed with form processing...
```

## Proteger endpoints de API

También puede proteger endpoints de API con Turnstile. Esto es útil para prevenir el abuso automatizado de APIs públicas.

Cree `src/pages/api/contact.ts`:

```typescript
// File: src/pages/api/contact.ts

import type { APIRoute } from 'astro'
import { verifyTurnstileToken } from '../../lib/turnstile'

export const POST: APIRoute = async ({ request, locals }) => {
  try {
    const body = await request.json()
    const { email, message, token } = body

    // Validate required fields
    if (!email || !message) {
      return new Response(
        JSON.stringify({ error: 'Email and message are required' }),
        { status: 400, headers: { 'Content-Type': 'application/json' } }
      )
    }

    // Verify Turnstile token
    if (!token) {
      return new Response(
        JSON.stringify({ error: 'Verification token missing' }),
        { status: 400, headers: { 'Content-Type': 'application/json' } }
      )
    }

    const secretKey = locals.runtime.env.TURNSTILE_SECRET_KEY
    const clientIP = request.headers.get('CF-Connecting-IP') || undefined

    const verification = await verifyTurnstileToken(token, secretKey, clientIP)

    if (!verification.success) {
      return new Response(
        JSON.stringify({
          error: 'Verification failed',
          codes: verification['error-codes'],
        }),
        { status: 403, headers: { 'Content-Type': 'application/json' } }
      )
    }

    // Process the contact form (e.g., send email, save to database)
    console.log('Valid contact form submission:', email)

    return new Response(
      JSON.stringify({ success: true, message: 'Message received' }),
      { status: 200, headers: { 'Content-Type': 'application/json' } }
    )
  } catch (error) {
    console.error('Contact API error:', error)
    return new Response(
      JSON.stringify({ error: 'Internal server error' }),
      { status: 500, headers: { 'Content-Type': 'application/json' } }
    )
  }
}
```

Formulario frontend que llama a esta API:

```astro
---
// File: src/pages/contact.astro

const siteKey = import.meta.env.PROD 
  ? Astro.locals.runtime.env.TURNSTILE_SITE_KEY 
  : import.meta.env.TURNSTILE_SITE_KEY;
---

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Contact Us</title>
    <script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
  </head>
  <body>
    <h1>Contact Us</h1>
    <form id="contact-form">
      <input type="email" name="email" placeholder="Your email" required />
      <textarea name="message" placeholder="Your message" required></textarea>
      <div class="cf-turnstile" data-sitekey={siteKey}></div>
      <button type="submit">Send Message</button>
    </form>
    <script>
      const form = document.getElementById('contact-form');
      form?.addEventListener('submit', async (e) => {
        e.preventDefault();
        const formData = new FormData(form);
        const token = formData.get('cf-turnstile-response');
        const response = await fetch('/api/contact', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            token,
            email: formData.get('email'),
            message: formData.get('message'),
          }),
        });
        const result = await response.json();
        if (result.success) {
          alert('Message sent successfully!');
          form.reset();
        } else 
          alert('Error: ' + result.error);
      });
    </script>
  </body>
</html>
```

## Personalización del widget

Turnstile ofrece varios modos de widget y temas:

### Modos de widget

```html
<!-- Managed (recommended): Shows challenge only when needed -->
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY"></div>

<!-- Non-Interactive: Invisible, always runs in background -->
<div class="cf-turnstile" data-sitekey="YOUR_SITE_KEY" data-appearance="interaction-only"></div>
```

### Callbacks

```html
<div 
  class="cf-turnstile" 
  data-sitekey="YOUR_SITE_KEY"
  data-callback="onTurnstileSuccess"
  data-error-callback="onTurnstileError"
  data-expired-callback="onTurnstileExpired"
></div>

<script>
  function onTurnstileSuccess(token) {
    console.log('Turnstile verified:', token);
  }

  function onTurnstileError(error) {
    console.error('Turnstile error:', error);
  }

  function onTurnstileExpired() {
    console.warn('Turnstile token expired');
  }
</script>
```

## Manejo de errores

[Códigos de error comunes de Turnstile](https://developers.cloudflare.com/turnstile/get-started/server-side-validation/#error-codes-reference):

| Código de error | Significado | Solución |
|-----------------|-------------|----------|
| `missing-input-secret` | No se proporcionó la clave secreta | Compruebe las variables de entorno |
| `invalid-input-secret` | Clave secreta no válida | Verifique su clave secreta |
| `missing-input-response` | No se proporcionó el token | Asegúrese de que el formulario envíe el token |
| `invalid-input-response` | Token no válido o caducado | Pida al usuario que reintente la verificación |
| `timeout-or-duplicate` | Token ya usado o caducado | Genere un token nuevo |

Gestione los errores con elegancia:

```typescript
const verification = await verifyTurnstileToken(token, secretKey)

if (!verification.success) {
  const errorCode = verification['error-codes']?.[0]
  switch (errorCode) {
    case 'timeout-or-duplicate':
      return new Response('Token expired. Please refresh the page.', { status: 400 })
    case 'invalid-input-response':
      return new Response('Invalid verification. Please try again.', { status: 400 })
    default:
      return new Response('Verification failed. Please contact support.', { status: 500 })
  }
}
```

## Desplegar en Cloudflare Workers

Despliegue su aplicación protegida contra bots en producción:

```bash
# Build the project
npm run build

# Deploy to Cloudflare Workers
npx wrangler deploy
```

Sus formularios ya están protegidos en producción.

## Buenas prácticas

- **Verifique siempre en el servidor**: Nunca confíe únicamente en la validación del lado del cliente.
- **Use el modo managed**: Deje que Turnstile decida cuándo mostrar desafíos.
- **Gestione los errores con elegancia**: Muestre mensajes comprensibles para el usuario, no códigos de error sin procesar.
- **Combine con limitación de tasa**: Añada [limitación de tasa](/blog/rate-limiting-astro-cloudflare-workers) para una protección adicional.

## Conclusión

Cloudflare Turnstile sustituye los CAPTCHA frustrantes por una protección invisible contra bots que prioriza la privacidad. Al integrar Turnstile en sus formularios y endpoints de API de Astro, bloquea el abuso automatizado sin degradar la experiencia de usuario para visitantes legítimos.

El patrón que ha construido, widget en el cliente + verificación en el servidor + manejo elegante de errores, escala fácilmente a cualquier formulario o API. Añádalo a páginas de inicio de sesión, flujos de pago, secciones de comentarios o cualquier endpoint que necesite protección contra bots.

Si tiene preguntas o comentarios, no dude en contactarme en [Twitter](https://twitter.com/direct_messages/create/rishi_raj_jain_).
