
Desplegando una App Next.js (App Router) con Server Actions en Cloudflare Pages (Funciones Incluidas)
Si has seguido la evolución de Next.js y has experimentado con el nuevo App Router, seguramente te has topado con las Server Actions. Son una característica potente que permite ejecutar código del servidor directamente desde componentes cliente, simplificando enormemente el manejo de formularios y mutaciones de datos.
Hasta hace poco, desplegar aplicaciones Next.js con estas características modernas en Cloudflare Pages era complicado o directamente imposible. Sin embargo, con las últimas mejoras en el soporte de Cloudflare para Next.js y las funciones serverless, ahora es posible y sorprendentemente sencillo.
En este tutorial te muestro paso a paso cómo desplegar una aplicación Next.js completa con App Router y Server Actions en Cloudflare Pages, aprovechando al máximo la infraestructura global de Cloudflare.
💡 Repositorio Demo: Puedes acceder al código completo de este tutorial en GitHub: nextjs-cloudflare-demo. El repositorio incluye toda la configuración y código necesario para seguir el tutorial.
¿Por qué Cloudflare Pages para Next.js?
Antes de saltar al tutorial, déjame explicarte por qué considero que Cloudflare Pages es una excelente opción para alojar aplicaciones Next.js en 2025:
Ventajas de Cloudflare Pages
- Red Global: Más de 275+ ubicaciones en todo el mundo significa que tu aplicación estará cerca de tus usuarios.
- Funciones Serverless Incluidas: Soporte nativo para Server Actions y API Routes sin configuración adicional.
- Precios Competitivos: Plan gratuito muy generoso, ideal para proyectos personales y startups.
- Rendimiento Excelente: CDN integrado y optimizaciones automáticas.
- Facilidad de Despliegue: Integración directa con GitHub y GitLab.
Como mencioné en mi experiencia moviendo proyectos a Cloudflare, he tenido muy buenas experiencias con su plataforma, especialmente en términos de rendimiento y costos.
Requisitos Previos
Para seguir este tutorial, necesitarás:
- Node.js 18+ instalado
- Una cuenta en Cloudflare
- Una cuenta en GitHub (para el despliegue automático)
- Conocimientos básicos de React y Next.js
- Familiaridad con el App Router de Next.js
Paso 1: Creando la Aplicación Next.js de Ejemplo
Tienes dos opciones para seguir este tutorial:
Opción A: Clonar el Repositorio Demo (Recomendado)
Si quieres empezar directamente con el código completo:
git clone https://github.com/siner/nextjs-cloudflare-demo.git
cd nextjs-cloudflare-demo
npm install
Opción B: Crear la Aplicación desde Cero
Vamos a crear una aplicación Next.js desde cero que incluya tanto el App Router como Server Actions. Esta será nuestra aplicación de prueba.
npx create-next-app@latest nextjs-cloudflare-demo
cd nextjs-cloudflare-demo
Durante la instalación, asegúrate de seleccionar:
- ✅ TypeScript
- ✅ ESLint
- ✅ Tailwind CSS
- ✅
src/
directory - ✅ App Router
- ❌ Turbopack (opcional, pero recomendado para desarrollo)
Paso 2: Configurando la Estructura del Proyecto
Vamos a crear una aplicación simple con un formulario que utilice Server Actions. Esta demostración incluirá:
- Una página principal con un formulario
- Server Actions para manejar el envío del formulario
- Gestión de estado para mostrar mensajes de éxito/error
Creando la Página Principal
Primero, vamos a modificar src/app/page.tsx
:
import { ContactForm } from '@/components/contact-form'
import { Suspense } from 'react'
export default function Home() {
return (
<main className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4">
<div className="max-w-2xl mx-auto">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
Next.js + Cloudflare Pages
</h1>
<p className="text-xl text-gray-600">
Demo de App Router con Server Actions desplegado en Cloudflare Pages
</p>
</div>
<div className="bg-white rounded-lg shadow-lg p-8">
<h2 className="text-2xl font-semibold text-gray-800 mb-6">
Formulario de Contacto
</h2>
<Suspense fallback={<div>Cargando formulario...</div>}>
<ContactForm />
</Suspense>
</div>
<div className="mt-8 text-center">
<p className="text-gray-500">
Este formulario utiliza Server Actions para procesar los datos
</p>
</div>
</div>
</main>
)
}
Creando las Server Actions
Ahora vamos a crear nuestras Server Actions. Crea el archivo src/app/actions.ts
:
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export type FormState = {
message: string
type: 'success' | 'error' | null
}
export async function submitContactForm(
prevState: FormState,
formData: FormData
): Promise<FormState> {
// Extraer datos del formulario
const name = formData.get('name') as string
const email = formData.get('email') as string
const message = formData.get('message') as string
// Validación básica
if (!name || !email || !message) {
return {
message: 'Todos los campos son obligatorios',
type: 'error'
}
}
// Validación de email básica
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) {
return {
message: 'Por favor, introduce un email válido',
type: 'error'
}
}
try {
// Simular procesamiento (aquí conectarías con tu base de datos, API externa, etc.)
await new Promise((resolve) => setTimeout(resolve, 1000))
// En un caso real, aquí guardarías en base de datos, enviarías email, etc.
console.log('Datos del formulario:', { name, email, message })
// Revalidar la página para mostrar los cambios
revalidatePath('/')
return {
message: `¡Gracias ${name}! Tu mensaje ha sido enviado correctamente.`,
type: 'success'
}
} catch (error) {
console.error('Error procesando formulario:', error)
return {
message: 'Error interno del servidor. Inténtalo de nuevo.',
type: 'error'
}
}
}
// Server Action alternativa que muestra redirect
export async function submitAndRedirect(formData: FormData) {
const name = formData.get('name') as string
if (!name) {
throw new Error('Nombre es requerido')
}
// Procesar datos...
await new Promise((resolve) => setTimeout(resolve, 500))
// Redirigir a página de éxito
redirect(`/success?name=${encodeURIComponent(name)}`)
}
Creando el Componente del Formulario
Crea src/components/contact-form.tsx
:
'use client'
import { useFormState, useFormStatus } from 'react-dom'
import { submitContactForm, type FormState } from '@/app/actions/contact-form'
const initialState: FormState = {
message: '',
type: null
}
function SubmitButton() {
const { pending } = useFormStatus()
return (
<button
type="submit"
disabled={pending}
className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-200"
>
{pending ? (
<>
<svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white inline"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Enviando...
</>
) : (
'Enviar Mensaje'
)}
</button>
)
}
export function ContactForm() {
const [state, formAction] = useFormState(submitContactForm, initialState)
return (
<form action={formAction} className="space-y-6">
<div>
<label
htmlFor="name"
className="block text-sm font-medium text-gray-700 mb-2"
>
Nombre completo
</label>
<input
type="text"
id="name"
name="name"
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Tu nombre"
/>
</div>
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-2"
>
Email
</label>
<input
type="email"
id="email"
name="email"
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="[email protected]"
/>
</div>
<div>
<label
htmlFor="message"
className="block text-sm font-medium text-gray-700 mb-2"
>
Mensaje
</label>
<textarea
id="message"
name="message"
rows={4}
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
placeholder="Escribe tu mensaje aquí..."
/>
</div>
<SubmitButton />
{state.message && (
<div
className={`p-4 rounded-lg ${
state.type === 'success'
? 'bg-green-50 text-green-800 border border-green-200'
: 'bg-red-50 text-red-800 border border-red-200'
}`}
>
<p className="text-sm font-medium">{state.message}</p>
</div>
)}
</form>
)
}
Creando la Página de Éxito (Opcional)
Crea src/app/success/page.tsx
:
import Link from 'next/link'
import { Suspense } from 'react'
function SuccessContent({ searchParams }: { searchParams: { name?: string } }) {
const name = searchParams.name || 'Usuario'
return (
<main className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100 py-12 px-4">
<div className="max-w-2xl mx-auto text-center">
<div className="bg-white rounded-lg shadow-lg p-8">
<div className="mb-6">
<div className="mx-auto w-16 h-16 bg-green-100 rounded-full flex items-center justify-center mb-4">
<svg
className="w-8 h-8 text-green-600"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">
¡Mensaje Enviado!
</h1>
<p className="text-xl text-gray-600">
Gracias {decodeURIComponent(name)}, tu mensaje ha sido procesado
correctamente.
</p>
</div>
<Link
href="/"
className="inline-block bg-blue-600 text-white py-3 px-6 rounded-lg hover:bg-blue-700 transition-colors duration-200"
>
Volver al Inicio
</Link>
</div>
</div>
</main>
)
}
export default async function SuccessPage({
searchParams
}: {
searchParams: Promise<{ name?: string }>
}) {
const params = await searchParams
return (
<Suspense fallback={<div>Cargando...</div>}>
<SuccessContent searchParams={params} />
</Suspense>
)
}
Paso 3: Configuración para Cloudflare Pages
Ahora viene la parte crucial: configurar Next.js para que funcione correctamente en Cloudflare Pages.
Instalando el Adaptador de Cloudflare
npm install @opennextjs/cloudflare
Nota:
@opennextjs/cloudflare
es la herramienta oficial actualizada para desplegar Next.js en Cloudflare Pages. Reemplaza al anterior@cloudflare/next-on-pages
y ofrece mejor soporte para las características modernas de Next.js como Server Actions y el App Router.
Configurando next.config.js
Crea o modifica next.config.js
en la raíz del proyecto:
/** @type {import('next').NextConfig} */
const nextConfig = {
// Optimizaciones para Cloudflare
images: {
unoptimized: true // Cloudflare optimiza las imágenes por defecto
},
// Configuración adicional para mejor compatibilidad
swcMinify: true,
// Headers de seguridad (opcional pero recomendado)
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
}
]
}
]
}
}
module.exports = nextConfig
Configuración de Package.json
Actualiza los scripts en tu package.json
:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"pages:build": "opennextjs-cloudflare build",
"preview": "npm run pages:build && opennextjs-cloudflare preview",
"deploy": "npm run pages:build && opennextjs-cloudflare deploy",
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
}
}
Configuración de OpenNext
Crea un archivo open-next.config.ts
en la raíz del proyecto:
import { defineCloudflareConfig } from '@opennextjs/cloudflare'
export default defineCloudflareConfig({
// Configuraciones específicas para tu proyecto
})
Paso 4: Preparando el Repositorio
Estructura Final del Proyecto
Tu proyecto debería tener esta estructura:
nextjs-cloudflare-demo/
├── src/
│ ├── app/
│ │ ├── actions.ts
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── success/
│ │ └── page.tsx
│ └── components/
│ └── ContactForm.tsx
├── next.config.js
├── open-next.config.ts
├── package.json
├── tailwind.config.js
└── tsconfig.json
Archivo .gitignore
Asegúrate de que tu .gitignore
incluya:
# Dependencies
node_modules/
# Next.js
.next/
out/
# OpenNext y Cloudflare
.open-next/
.wrangler/
# Environment variables
.env*.local
# Logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
Commiteando el Código
git add .
git commit -m "feat: Aplicación Next.js con App Router y Server Actions para Cloudflare Pages"
git push origin main
Desarrollo y Previsualización Local
Antes de desplegar, es importante probar localmente con el runtime de Cloudflare:
Desarrollo Regular
Para desarrollo regular, usa el servidor de Next.js:
npm run dev
Previsualización con Runtime de Cloudflare
Para probar cómo funcionará en producción:
npm run preview
Este comando:
- Construye tu aplicación con el adaptador de OpenNext
- La sirve localmente usando el runtime de
workers
(mismo que usa Cloudflare) - Te permite probar Server Actions y otras características específicas de Cloudflare
Paso 5: Configurando Cloudflare Pages
Creando el Proyecto en Cloudflare Pages
- Ve a Cloudflare Dashboard
- Navega a Pages en el menú lateral
- Haz clic en “Crear Proyecto”
- Selecciona “Conectar a Git”
- Autoriza a Cloudflare a acceder a tu repositorio de GitHub
- Selecciona tu repositorio
nextjs-cloudflare-demo
Configuración de Build
En la pantalla de configuración de build, usa estos ajustes:
- Framework preset: Next.js
- Build command:
npm run pages:build
- Build output directory:
.open-next/assets
(o.open-next
según la configuración) - Root directory:
/
(raíz del proyecto)
Variables de Entorno (Opcional)
Si tu aplicación necesita variables de entorno, puedes agregarlas en la sección “Variables de Entorno”:
NODE_VERSION=18
NEXT_TELEMETRY_DISABLED=1
Configuración Avanzada
En “Configuración Avanzada”, puedes ajustar:
- Compatibility flags:
nodejs_compat
(para Server Actions) - Node.js compatibility: Habilitado
Paso 6: Despliegue y Verificación
Iniciando el Despliegue
Haz clic en “Guardar y Desplegar”. Cloudflare comenzará a:
- Clonar tu repositorio
- Instalar dependencias (
npm install
) - Ejecutar el build (
npm run pages:build
) - Desplegar a la red global de Cloudflare
Monitoreando el Progreso
Puedes seguir el progreso del despliegue en tiempo real. El proceso típicamente toma 2-5 minutos.
Verificando la Funcionalidad
Una vez completado el despliegue:
- Visita tu sitio: Cloudflare te proporcionará una URL (ej:
https://nextjs-cloudflare-demo.pages.dev
) - Prueba el formulario: Completa y envía el formulario de contacto
- Verifica las Server Actions: El formulario debería procesarse correctamente en el servidor
- Revisa las funciones: En el dashboard de Cloudflare, ve a Pages > Tu Proyecto > Funciones para ver las estadísticas de uso
Paso 7: Configuración de Dominio Personalizado (Opcional)
Agregando tu Dominio
Si tienes un dominio personalizado:
- Ve a Pages > Tu Proyecto > Configuración
- Haz clic en “Dominios Personalizados”
- Haz clic en “Configurar dominio personalizado”
- Introduce tu dominio (ej:
mi-app.midominio.com
) - Sigue las instrucciones para configurar los registros DNS
Configurando SSL
Cloudflare proporciona SSL automáticamente. Para dominios personalizados:
- Asegúrate de que tu dominio esté gestionado por Cloudflare
- Habilita “SSL/TLS Encryption Mode: Full”
- Activa “Always Use HTTPS”
Optimizaciones y Mejores Prácticas
Performance
- Caching: Cloudflare cachea automáticamente assets estáticos
- Minification: Habilitada por defecto para CSS, JS y HTML
- Brotli Compression: Automática para mejor compresión
Seguridad
// next.config.js - Headers de seguridad adicionales
async headers() {
return [
{
source: '/(.*)',
headers: [
{
key: 'Strict-Transport-Security',
value: 'max-age=31536000; includeSubDomains'
},
{
key: 'Content-Security-Policy',
value: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'"
}
]
}
]
}
Monitoreo
- Analytics: Habilita Cloudflare Web Analytics
- Real User Monitoring: Para métricas de rendimiento real
- Logs: Revisa logs de funciones para debugging
Troubleshooting Común
Problemas con Server Actions
Error: “Server Actions not working” Solución:
- Verifica que
nodejs_compat
esté habilitado - Asegúrate de usar
'use server'
al inicio de tus actions - Revisa los logs de funciones en Cloudflare
Build Failures
Error: “Build failed” Solución:
- Verifica que
@opennextjs/cloudflare
esté instalado - Usa Node.js 18+
- Revisa la configuración de
next.config.js
Runtime Errors
Error: “Runtime errors in production” Solución:
- Revisa los logs de Pages Functions
- Verifica compatibilidad de dependencias con Workers Runtime
- Considera usar polyfills si es necesario
Limitaciones a Considerar
Compatibilidad del Adaptador OpenNext
-
Compatibilidad con Windows: Actualmente,
@opennextjs/cloudflare
no es totalmente compatible con Windows. Se espera compatibilidad completa en la versión 1.0. -
Runtime de Edge: El adaptador actualmente solo soporta el runtime de Node.js de Next.js. El soporte para Edge Runtime está planificado para la próxima versión principal.
Workers Runtime Limitations
- CPU Time: Límite de 30 segundos por request en plan gratuito
- Memory: Límite de memoria más restrictivo que Node.js tradicional
- APIs: Algunas APIs de Node.js no están disponibles en el runtime de Workers
Next.js Features
- Middleware: Soporte limitado, usa Workers en su lugar
- Custom Server: No soportado, usa funciones en su lugar
- Incremental Static Regeneration: Funcionalidad limitada
Conclusión
Desplegar Next.js con App Router y Server Actions en Cloudflare Pages usando @opennextjs/cloudflare
es ahora más accesible y robusto que nunca. Este adaptador oficial, desarrollado por OpenNext, ofrece mejor compatibilidad y soporte para las características modernas de Next.js.
La combinación de la red global de Cloudflare, las funciones serverless integradas y el soporte mejorado para Next.js hace de esta una opción muy atractiva para aplicaciones modernas, especialmente considerando que OpenNext se ha convertido en el estándar para desplegar Next.js en entornos serverless fuera de Vercel.
Ventajas Clave
- Rendimiento Global: Tu aplicación estará disponible desde +275 ubicaciones
- Costo Efectivo: Plan gratuito muy generoso
- Escalabilidad Automática: Sin configuración de infraestructura
- Simplicidad: Despliegue automático desde Git
- Funciones Incluidas: Server Actions funcionan out-of-the-box
Próximos Pasos
- Conecta una base de datos (considera Supabase o PlanetScale)
- Implementa autenticación con Better Auth
- Agrega análisis con Cloudflare Analytics
- Explora Cloudflare Workers para lógica adicional
Código Completo
Recuerda que puedes acceder al código completo de este tutorial en el repositorio de GitHub: nextjs-cloudflare-demo. Incluye:
- ✅ Configuración completa de OpenNext para Cloudflare
- ✅ Aplicación Next.js con App Router y Server Actions
- ✅ Formulario de contacto funcional con validación
- ✅ Componentes con manejo de estado y loading
- ✅ Configuración de despliegue optimizada
- ✅ Ejemplos de uso de Suspense y async components
¿Has probado Next.js en Cloudflare Pages? ¿Qué tal ha sido tu experiencia? ¡Me encantaría conocer tus casos de uso y cualquier truco adicional que hayas descubierto!
Si quieres explorar otras opciones de hosting o comparar con otros stacks, puedes revisar mi análisis de herramientas de desarrollo fullstack o mi experiencia personal con Next.js vs Astro.