Avanzado y Buenas Prácticas en Next.js

Domina los conceptos avanzados de Next.js con ejemplos prácticos y sencillos. Esta guía cubre Server Actions, Middleware, Autenticación y técnicas de optimización.

1. Server Actions (“use server”)

¿Qué son los Server Actions?

Los Server Actions permiten ejecutar código del servidor directamente desde componentes del cliente sin crear APIs separadas. Se ejecutan de forma segura en el servidor.

Ejemplo Básico

javascript
// app/actions/user-actions.js
'use server'

import { redirect } from 'next/navigation'

// Server Action para crear usuario
export async function createUser(formData) {
const name = formData.get('name')
const email = formData.get('email')

// Simular guardado en base de datos
console.log('Guardando usuario:', { name, email })

// Redireccionar después de guardar
redirect('/users')
}

// Server Action para eliminar usuario
export async function deleteUser(userId) {
console.log('Eliminando usuario:', userId)

// Simular eliminación
return { success: true, message: 'Usuario eliminado' }
}

Usar Server Actions en Componentes

javascript
// app/users/create/page.js
import { createUser } from '@/app/actions/user-actions'

export default function CreateUserPage() {
return (
  <div className="max-w-md mx-auto p-6">
    <h1 className="text-2xl font-bold mb-6">Crear Usuario</h1>
    
    {/* Formulario que usa Server Action directamente */}
    <form action={createUser} className="space-y-4">
      <div>
        <label className="block text-sm font-medium mb-1">
          Nombre
        </label>
        <input
          name="name"
          type="text"
          required
          className="w-full px-3 py-2 border rounded-md"
        />
      </div>
      
      <div>
        <label className="block text-sm font-medium mb-1">
          Email
        </label>
        <input
          name="email"
          type="email"
          required
          className="w-full px-3 py-2 border rounded-md"
        />
      </div>
      
      <button
        type="submit"
        className="w-full bg-blue-500 text-white py-2 rounded-md hover:bg-blue-600"
      >
        Crear Usuario
      </button>
    </form>
  </div>
)
}

Server Action con useFormState

javascript
// app/components/UserFormWithState.js
'use client'
import { useFormState } from 'react-dom'

// Server Action que retorna estado
async function createUserWithState(prevState, formData) {
'use server'

const name = formData.get('name')
const email = formData.get('email')

if (!name || !email) {
  return { error: 'Todos los campos son requeridos' }
}

// Simular guardado
return { success: true, message: 'Usuario creado correctamente' }
}

export default function UserFormWithState() {
const [state, formAction] = useFormState(createUserWithState, null)

return (
  <form action={formAction} className="space-y-4">
    {state?.error && (
      <div className="bg-red-100 text-red-700 p-3 rounded">
        {state.error}
      </div>
    )}
    
    {state?.success && (
      <div className="bg-green-100 text-green-700 p-3 rounded">
        {state.message}
      </div>
    )}
    
    <input name="name" placeholder="Nombre" className="w-full p-2 border rounded" />
    <input name="email" placeholder="Email" className="w-full p-2 border rounded" />
    <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">
      Crear
    </button>
  </form>
)
}
 

Los Server Actions solo funcionan en Next.js 13+ con App Router. Siempre usa ‘use server’ al inicio del archivo o función.

2. Middleware

¿Qué es el Middleware?

El Middleware se ejecuta antes de que se complete una request. Útil para autenticación, redirecciones, headers personalizados y logging.

Configuración Básica

javascript
// middleware.js (en la raíz del proyecto)
import { NextResponse } from 'next/server'

export function middleware(request) {
// Obtener la URL actual
const { pathname } = request.nextUrl

// Ejemplo: Redireccionar /admin a /dashboard
if (pathname === '/admin') {
  return NextResponse.redirect(new URL('/dashboard', request.url))
}

// Ejemplo: Agregar header personalizado
const response = NextResponse.next()
response.headers.set('X-Custom-Header', 'Mi-App-Next')

return response
}

// Configurar en qué rutas se ejecuta
export const config = {
matcher: [
  '/((?!api|_next/static|_next/image|favicon.ico).*)',
]
}

Middleware para Autenticación

javascript
// middleware.js
import { NextResponse } from 'next/server'

export function middleware(request) {
const { pathname } = request.nextUrl

// Rutas protegidas
const protectedRoutes = ['/dashboard', '/profile', '/admin']
const isProtectedRoute = protectedRoutes.some(route => 
  pathname.startsWith(route)
)

if (isProtectedRoute) {
  // Verificar si hay token de autenticación
  const token = request.cookies.get('auth-token')
  
  if (!token) {
    // Redireccionar a login si no hay token
    return NextResponse.redirect(new URL('/login', request.url))
  }
}

// Redireccionar usuarios autenticados lejos del login
if (pathname === '/login') {
  const token = request.cookies.get('auth-token')
  if (token) {
    return NextResponse.redirect(new URL('/dashboard', request.url))
  }
}

return NextResponse.next()
}

export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*', '/admin/:path*', '/login']
}

Middleware con Geolocalización

javascript
// middleware.js
import { NextResponse } from 'next/server'

export function middleware(request) {
// Obtener país del usuario
const country = request.geo?.country || 'US'
const { pathname } = request.nextUrl

// Redireccionar según el país
if (pathname === '/' && country === 'ES') {
  return NextResponse.redirect(new URL('/es', request.url))
}

if (pathname === '/' && country === 'MX') {
  return NextResponse.redirect(new URL('/mx', request.url))
}

// Agregar país como header
const response = NextResponse.next()
response.headers.set('X-User-Country', country)

return response
}

3. Autenticación con NextAuth

Instalación y Configuración

bash
# Instalar NextAuth
npm install next-auth

# Instalar adaptadores (opcional)
npm install @auth/prisma-adapter prisma

Configuración Básica de NextAuth

javascript
// app/api/auth/[...nextauth]/route.js
import NextAuth from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
import CredentialsProvider from 'next-auth/providers/credentials'

const handler = NextAuth({
providers: [
  // Proveedor de Google
  GoogleProvider({
    clientId: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
  }),
  
  // Proveedor de credenciales (email/password)
  CredentialsProvider({
    name: 'credentials',
    credentials: {
      email: { label: 'Email', type: 'email' },
      password: { label: 'Password', type: 'password' }
    },
    async authorize(credentials) {
      // Aquí validarías las credenciales
      if (credentials?.email === 'admin@test.com' && 
          credentials?.password === '123456') {
        return {
          id: '1',
          name: 'Admin User',
          email: 'admin@test.com',
        }
      }
      return null
    }
  })
],

pages: {
  signIn: '/login',
  signOut: '/logout',
},

callbacks: {
  async jwt({ token, user }) {
    if (user) {
      token.role = user.role || 'user'
    }
    return token
  },
  
  async session({ session, token }) {
    session.user.role = token.role
    return session
  }
}
})

export { handler as GET, handler as POST }

Componente de Login

javascript
// app/login/page.js
'use client'
import { signIn, getSession } from 'next-auth/react'
import { useState } from 'react'

export default function LoginPage() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)

const handleSubmit = async (e) => {
  e.preventDefault()
  setLoading(true)
  
  const result = await signIn('credentials', {
    email,
    password,
    redirect: false,
  })
  
  if (result?.ok) {
    window.location.href = '/dashboard'
  } else {
    alert('Credenciales incorrectas')
  }
  
  setLoading(false)
}

return (
  <div className="max-w-md mx-auto p-6">
    <h1 className="text-2xl font-bold mb-6">Iniciar Sesión</h1>
    
    <form onSubmit={handleSubmit} className="space-y-4">
      <input
        type="email"
        placeholder="Email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        className="w-full p-3 border rounded"
        required
      />
      
      <input
        type="password"
        placeholder="Contraseña"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        className="w-full p-3 border rounded"
        required
      />
      
      <button
        type="submit"
        disabled={loading}
        className="w-full bg-blue-500 text-white p-3 rounded hover:bg-blue-600"
      >
        {loading ? 'Cargando...' : 'Iniciar Sesión'}
      </button>
    </form>
    
    <div className="mt-4">
      <button
        onClick={() => signIn('google')}
        className="w-full bg-red-500 text-white p-3 rounded hover:bg-red-600"
      >
        Continuar con Google
      </button>
    </div>
  </div>
)
}

Proteger Páginas con NextAuth

javascript
// app/dashboard/page.js
import { getServerSession } from 'next-auth'
import { redirect } from 'next/navigation'

export default async function DashboardPage() {
const session = await getServerSession()

if (!session) {
  redirect('/login')
}

return (
  <div className="p-6">
    <h1 className="text-2xl font-bold mb-4">Dashboard</h1>
    <p>Bienvenido, {session.user?.name}!</p>
    <p>Email: {session.user?.email}</p>
  </div>
)
}

4. Variables de Entorno

Configuración de .env.local

bash
# .env.local
# Base de datos
DATABASE_URL="mysql://user:password@localhost:3306/mydb"

# NextAuth
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="mi-secreto-super-seguro"

# Google OAuth
GOOGLE_CLIENT_ID="tu-google-client-id"
GOOGLE_CLIENT_SECRET="tu-google-client-secret"

# API Keys
API_KEY="mi-api-key-secreta"
STRIPE_SECRET_KEY="sk_test_..."

# Variables públicas (prefijo NEXT_PUBLIC_)
NEXT_PUBLIC_APP_NAME="Mi App"
NEXT_PUBLIC_API_URL="https://api.miapp.com"

Usar Variables de Entorno

javascript
// lib/config.js
export const config = {
// Variables del servidor (seguras)
databaseUrl: process.env.DATABASE_URL,
apiKey: process.env.API_KEY,
stripeSecret: process.env.STRIPE_SECRET_KEY,

// Variables públicas (accesibles en el cliente)
appName: process.env.NEXT_PUBLIC_APP_NAME,
apiUrl: process.env.NEXT_PUBLIC_API_URL,
}

// Ejemplo de uso en API Route
// app/api/users/route.js
export async function GET() {
const apiKey = process.env.API_KEY

const response = await fetch('https://external-api.com/users', {
  headers: {
    'Authorization': `Bearer ${apiKey}`
  }
})

return Response.json(await response.json())
}

Variables de Entorno por Ambiente

bash
# .env.local (desarrollo)
NEXT_PUBLIC_API_URL="http://localhost:3000/api"
DATABASE_URL="mysql://root:password@localhost:3306/dev_db"

# .env.production (producción)
NEXT_PUBLIC_API_URL="https://miapp.com/api"
DATABASE_URL="mysql://user:pass@prod-server:3306/prod_db"

# .env.test (testing)
NEXT_PUBLIC_API_URL="http://localhost:3001/api"
DATABASE_URL="mysql://test:test@localhost:3306/test_db"
 

Solo las variables con prefijo NEXT_PUBLIC_ son accesibles en el cliente. Las demás solo están disponibles en el servidor.

5. Configuración de Headers y Revalidación

Fetch con Revalidación

javascript
// app/posts/page.js
async function getPosts() {
// Revalidar cada 60 segundos
const res = await fetch('https://api.example.com/posts', {
  next: { revalidate: 60 }
})

if (!res.ok) {
  throw new Error('Error al obtener posts')
}

return res.json()
}

export default async function PostsPage() {
const posts = await getPosts()

return (
  <div>
    <h1>Posts (Actualizado cada 60s)</h1>
    {posts.map(post => (
      <div key={post.id} className="border p-4 mb-4">
        <h2>{post.title}</h2>
        <p>{post.content}</p>
      </div>
    ))}
  </div>
)
}

Headers Personalizados

javascript
// app/api/data/route.js
export async function GET() {
const data = await fetch('https://external-api.com/data', {
  headers: {
    'Authorization': 'Bearer ' + process.env.API_TOKEN,
    'Content-Type': 'application/json',
    'User-Agent': 'Mi-App/1.0'
  },
  next: { 
    revalidate: 300, // 5 minutos
    tags: ['external-data'] // Para revalidación por tags
  }
})

return Response.json(await data.json(), {
  headers: {
    'Cache-Control': 'public, s-maxage=300',
    'X-Custom-Header': 'Mi-Valor'
  }
})
}

Configuración Global de Headers

javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
  return [
    {
      source: '/api/:path*',
      headers: [
        {
          key: 'Access-Control-Allow-Origin',
          value: '*',
        },
        {
          key: 'Access-Control-Allow-Methods',
          value: 'GET, POST, PUT, DELETE, OPTIONS',
        },
        {
          key: 'X-Content-Type-Options',
          value: 'nosniff',
        },
      ],
    },
    {
      source: '/:path*',
      headers: [
        {
          key: 'X-Frame-Options',
          value: 'DENY',
        },
      ],
    },
  ]
},
}

module.exports = nextConfig

6. ISR (Incremental Static Regeneration)

¿Qué es ISR?

ISR permite regenerar páginas estáticas en tiempo de ejecución sin reconstruir toda la aplicación. Combina los beneficios de SSG y SSR.

ISR Básico

javascript
// app/products/[id]/page.js
async function getProduct(id) {
const res = await fetch(`https://api.store.com/products/${id}`, {
  next: { 
    revalidate: 3600 // Revalidar cada hora
  }
})

if (!res.ok) {
  throw new Error('Producto no encontrado')
}

return res.json()
}

export default async function ProductPage({ params }) {
const product = await getProduct(params.id)

return (
  <div className="p-6">
    <h1 className="text-3xl font-bold">{product.name}</h1>
    <p className="text-gray-600 mt-2">{product.description}</p>
    <p className="text-2xl font-bold mt-4">${product.price}</p>
    
    <div className="mt-4 text-sm text-gray-500">
      Última actualización: {new Date().toLocaleString()}
    </div>
  </div>
)
}

// Generar páginas estáticas para productos populares
export async function generateStaticParams() {
const products = await fetch('https://api.store.com/products/popular')
  .then(res => res.json())

return products.map(product => ({
  id: product.id.toString()
}))
}

ISR con Diferentes Estrategias

javascript
// app/blog/[slug]/page.js
async function getPost(slug) {
const res = await fetch(`https://cms.example.com/posts/${slug}`, {
  next: { 
    revalidate: 86400, // 24 horas
    tags: ['posts', `post-${slug}`] // Tags para revalidación específica
  }
})

return res.json()
}

export default async function BlogPost({ params }) {
const post = await getPost(params.slug)

return (
  <article className="max-w-4xl mx-auto p-6">
    <h1 className="text-4xl font-bold mb-4">{post.title}</h1>
    <div className="text-gray-600 mb-6">
      Publicado: {new Date(post.publishedAt).toLocaleDateString()}
    </div>
    <div className="prose max-w-none">
      {post.content}
    </div>
  </article>
)
}

// ISR con fallback
export async function generateStaticParams() {
// Solo generar las 10 páginas más populares
const posts = await fetch('https://cms.example.com/posts?limit=10')
  .then(res => res.json())

return posts.map(post => ({
  slug: post.slug
}))
}

7. Revalidación On-Demand

revalidatePath

javascript
// app/actions/blog-actions.js
'use server'
import { revalidatePath } from 'next/cache'

export async function updatePost(postId, formData) {
const title = formData.get('title')
const content = formData.get('content')

// Actualizar post en la base de datos
await fetch(`https://cms.example.com/posts/${postId}`, {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title, content })
})

// Revalidar la página específica del post
revalidatePath(`/blog/${postId}`)

// Revalidar la página de lista de posts
revalidatePath('/blog')
}

revalidateTag

javascript
// app/actions/product-actions.js
'use server'
import { revalidateTag } from 'next/cache'

export async function updateProduct(productId, data) {
// Actualizar producto
await fetch(`https://api.store.com/products/${productId}`, {
  method: 'PUT',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(data)
})

// Revalidar todas las páginas que usan el tag 'products'
revalidateTag('products')

// Revalidar tag específico del producto
revalidateTag(`product-${productId}`)
}

// En la función de fetch, usar tags
async function getProducts() {
const res = await fetch('https://api.store.com/products', {
  next: { 
    revalidate: 3600,
    tags: ['products'] // Este tag se puede revalidar
  }
})

return res.json()
}

API Route para Revalidación Manual

javascript
// app/api/revalidate/route.js
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextResponse } from 'next/server'

export async function POST(request) {
const { path, tag, secret } = await request.json()

// Verificar token de seguridad
if (secret !== process.env.REVALIDATE_SECRET) {
  return NextResponse.json(
    { error: 'Token inválido' },
    { status: 401 }
  )
}

try {
  if (path) {
    revalidatePath(path)
    return NextResponse.json({ 
      message: `Página ${path} revalidada` 
    })
  }
  
  if (tag) {
    revalidateTag(tag)
    return NextResponse.json({ 
      message: `Tag ${tag} revalidado` 
    })
  }
  
  return NextResponse.json(
    { error: 'Se requiere path o tag' },
    { status: 400 }
  )
} catch (error) {
  return NextResponse.json(
    { error: 'Error al revalidar' },
    { status: 500 }
  )
}
}
 

ISR es ideal para sitios con contenido que cambia ocasionalmente. Combina la velocidad de páginas estáticas con la flexibilidad de contenido dinámico.

 

Usa revalidateTag para invalidar múltiples páginas relacionadas de una vez, y revalidatePath para páginas específicas.

 

Las variables de entorno sensibles nunca deben tener el prefijo NEXT_PUBLIC_. Mantenlas seguras en el servidor.