From n00b to ZeroCool / Profesionalización

Next.js y el regreso del servidor: deja de pelearte con el frontend solo

Aprende Next.js con App Router: SSR/SSG/ISR, Server Components, rutas, data fetching, auth y deploy. Tips y errores comunes.

Lo que vale la pena leer aquí

Son las 11:47 pm. Mañana hay demo. Tu SPA “se ve bonita”… hasta que abres DevTools y ves 3MB de JS, Lighthouse en rojo y el SEO en modo fantasma.

Son las 11:47 pm. Mañana hay demo. Tu SPA “se ve bonita”… hasta que abres DevTools y ves 3MB de JS, Lighthouse en rojo y el SEO en modo fantasma.

Y obvio cae el mensaje del PM/jefe: “¿Por qué Google no encuentra nada?”

Ahí es cuando mucha banda se pone a meter otra librería, otro patch, otro workaround. Hasta que te topas con Next.js y su promesa medio rara: “React, pero con servidor otra vez”. Suena a retroceso… hasta que te toca un deploy tarde y te salva el sprint.

Qué te vas a llevar

  • Por qué Next.js no es “React con esteroides”, sino un regreso pragmático al server.
  • Cómo pensar el App Router sin hacerte bolas.
  • Cuándo conviene SSR, SSG, ISR (y cuándo es pura talacha inútil).
  • Data fetching sin duplicar llamadas: Server Components, fetch con caché y reglas claras.
  • Workflow de producción: rutas, layouts, errores, deploy y checks que sí se usan.

Por qué el server volvió (y no, no es nostalgia)

Si vienes de SPA pura (Vite + React + todo client-side), seguro ya te pegó esto:

  • El primer render depende de bajar JS + hidratar. En una laptop vieja con Chrome tragón, se siente.
  • SEO: Google “puede” ejecutar JS, pero cuando el negocio solo necesita que funcione (y el tráfico viene de búsquedas locales), apostar todo a eso es vivir al límite.
  • Auth: tokens por todos lados, refresh, storage, edge cases… y el día que se vence algo en production, nadie duerme.
  • Datos y secretos: pegas a APIs desde el browser, expones endpoints, y un día aparece la clásica: “¿quién subió la API key al bundle?”

Next.js te deja elegir: render en servidor cuando conviene, generar estático cuando se puede, y mandar al cliente solo lo necesario.

Dos escenas de jale real:

  • La app corre en un comedor con Telmex y tres dispositivos colgados del Wi‑Fi. El “en mi máquina carga” se muere. SSR/SSG ayuda.
  • Un cliente de e‑commerce te pide “salir en Google con refacciones en Querétaro” y tú con SPA: “pues… a veces indexa”. Con Next, la página ya sale renderizada.

Next.js (App Router) como se usa en producción

1) Crea el proyecto con App Router y TypeScript

npx create-next-app@latest mi-next
# Si te pregunta: App Router = yes, TypeScript = yes
cd mi-next
npm run dev

Estructura típica:

  • app/ → rutas y layouts
  • app/page.tsx → home
  • app/layout.tsx → layout raíz

Decisión con filo: si tu equipo está casado con React Router y carpetas por “features”, lo que pega al inicio es la ruta por filesystem. Pero también es lo que más orden mete: carpeta = URL. Menos inventos.

2) Mental model: Server Components por default

En App Router, lo que está dentro de app/ es Server Component por defecto.

  • Corre en servidor.
  • No trae useState ni useEffect.
  • Puede leer datos directo (DB, APIs internas) sin filtrar secretos al cliente.

Si necesitas interactividad, declaras:

'use client'

al inicio del archivo.

Tradeoff real:

  • Server Components = menos JS al navegador, mejor performance.
  • Client Components = UX interactiva, pero crece el bundle.

Regla que sí aguanta deadlines: todo lo que sea “mostrar info”, server. Lo que sea “usuario picando cosas”, client… pero chiquito.

3) Rutas, layouts y loading: el setup que evita pantallas blancas

Ejemplo de rutas:

  • app/productos/page.tsx/productos
  • app/productos/[id]/page.tsx/productos/123

Layouts:

// app/productos/layout.tsx
export default function ProductosLayout({ children }: { children: React.ReactNode }) {
  return (
    <section>
      <h1>Productos</h1>
      <div>{children}</div>
    </section>
  )
}

Loading UI:

// app/productos/loading.tsx
export default function Loading() {
  return <p>Cargando productos…</p>
}

Advertencia práctica: mete loading.tsx en rutas donde la red falla o el endpoint a veces se tarda porque alguien corre un reporte en la misma DB. No es “hacer bonito”; es quitar fricción.

4) Data fetching con fetch (caché, revalidate y errores)

En Server Components puedes hacer esto:

// app/productos/page.tsx
type Producto = { id: string; nombre: string }

async function getProductos(): Promise<Producto[]> {
  const res = await fetch('https://mi-api.com/productos', {
    // Next cachea por default en server cuando puede
    next: { revalidate: 60 }, // ISR: refresca cada 60s
  })
  if (!res.ok) throw new Error('Fallo al cargar productos')
  return res.json()
}

export default async function Page() {
  const productos = await getProductos()
  return (
    <ul>
      {productos.map(p => (
        <li key={p.id}>{p.nombre}</li>
      ))}
    </ul>
  )
}

Cómo decidir SSG/ISR/SSR sin drama:

  • SSG (estático): marketing, docs, catálogo que cambia poco.
  • ISR (revalidate): catálogo que cambia “cada rato”, sin tirar performance.
  • SSR (por request): dashboards, contenido por usuario, precios por cliente.

SSR explícito:

export const dynamic = 'force-dynamic'

O evitar caché en un fetch:

await fetch(url, { cache: 'no-store' })

Fricción real: si pones no-store por miedo “para que no falle”, te vuelas los beneficios. Pregunta simple: “¿esto necesita estar exacto al segundo?” Si no, ISR.

Next.js y el regreso del servidor: deja de pelearte con el frontend solo - visual explicativa 1
Visual de apoyo: Qué te vas a llevar

5) Interactividad: un Client Component bien colocado (sin convertir todo a client)

Página server + pieza client:

// app/productos/Filtro.tsx
'use client'

import { useState } from 'react'

export function Filtro({ onChange }: { onChange: (q: string) => void }) {
  const [q, setQ] = useState('')
  return (
    <input
      value={q}
      onChange={(e) => {
        const v = e.target.value
        setQ(v)
        onChange(v)
      }}
      placeholder="Buscar…"
    />
  )
}

Y en tu server page:

// app/productos/page.tsx
import { Filtro } from './Filtro'

export default async function Page() {
  // datos en server…
  return (
    <div>
      <Filtro onChange={() => { /* podrías usar URL params o router */ }} />
      {/* lista */}
    </div>
  )
}

Decisión que te ahorra bugs: si el filtro cambia el listado, muchas veces conviene mover el estado a la URL (search params). Así se puede compartir el link, no se pierde con refresh, y el workflow se siente más “pro”.

6) Form actions: el “regreso del servidor” donde más duele

Los forms son un pantano clásico: validación, loading, errores, API routes, CORS, tokens… Next te deja hacer server actions.

Ejemplo simple:

// app/contacto/actions.ts
'use server'

export async function enviarContacto(formData: FormData) {
  const email = String(formData.get('email') || '')
  const mensaje = String(formData.get('mensaje') || '')

  if (!email.includes('@') || mensaje.length < 10) {
    throw new Error('Datos inválidos')
  }

  // Aquí llamarías tu API interna o servicio
  // await fetch('https://...', { method: 'POST', body: JSON.stringify({ email, mensaje }) })
}
// app/contacto/page.tsx
import { enviarContacto } from './actions'

export default function Page() {
  return (
    <form action={enviarContacto}>
      <input name="email" placeholder="tu@mail.com" />
      <textarea name="mensaje" placeholder="¿En qué te ayudamos?" />
      <button type="submit">Enviar</button>
    </form>
  )
}

Tradeoff sin maquillaje:

  • Manejo de errores (no quieres un “500” sin feedback cuando el cliente está viendo)
  • Seguridad (validar SIEMPRE del lado servidor)
  • Observabilidad (logs, trace, lo mínimo para poder hacer rollback con datos)

7) Route handlers: APIs dentro del mismo repo (cuando sí conviene)

Endpoint interno:

// app/api/health/route.ts
export async function GET() {
  return Response.json({ ok: true, ts: Date.now() })
}

Cuándo sí:

  • BFF (backend-for-frontend) para no exponer APIs feas al browser
  • Integrar auth, cookies, headers, transformar data

Cuándo no:

  • Si ya tienes backend sólido y estás duplicando lógica
  • Si el equipo de backend te va a pedir cuentas por “otro backend” sin acuerdos

Screenshots sugeridos

  • Estructura del proyecto en VS Code: carpeta app/ con page.tsx, layout.tsx, loading.tsx.
  • Network tab comparando: SPA vs Next (ver HTML con contenido ya renderizado).
  • Ejemplo de next build y el output mostrando rutas estáticas/dinámicas.
  • Lighthouse antes/después (pero sin obsesionarte con 100/100 si rompes UX real).

Errores comunes y cómo salir vivo

1) “Tronó porque usé useState en un Server Component”

Síntoma: error pidiendo 'use client'.

Salida: mueve esa parte a un client component pequeño. Si conviertes toda la página a client “por rapidez”, vuelves a la SPA gigante con otro nombre.

2) “Estoy llamando dos veces a la API / se siente duplicado”

Causa típica: fetch en client y también en server, o re-renders por state.

Arreglo: define una sola fuente.

  • Si se puede, fetch en server y pasas props.
  • Si debe ser client (datos muy interactivos), entonces que sea client de verdad y evita duplicar.

3) “No se actualiza mi data, quedó cacheada”

Causa: fetch cacheando o ruta estática.

Salida: next: { revalidate: N } o cache: 'no-store' donde aplique. Y revisa si una ruta se te volvió estática “sin querer” por cómo quedó el código.

4) “Mi env var no existe en el cliente”

Regla: solo las que empiezan con NEXT_PUBLIC_ llegan al browser.

Decisión defensiva: si es secreto, NO debe estar en cliente. Léelo en server components, route handlers o server actions.

5) “Deploy en Vercel y se rompió, en local jalaba”

Causas comunes:

  • Versión de Node distinta
  • Variables de entorno faltantes
  • Rutas que asumían filesystem local

Salida: fija Node en package.json (engines), usa .env.example, y corre next build en CI antes del deploy.

Next.js y el regreso del servidor: deja de pelearte con el frontend solo - visual explicativa 2
Visual de apoyo: Por qué el server volvió (y no, no es nostalgia)

Checklist final (para profesionalizar tu setup)

  • Usas App Router y tienes claro qué corre en server vs client.
  • Las páginas “de SEO” renderizan contenido en servidor (SSG/ISR/SSR según necesidad).
  • Lo interactivo vive en Client Components chicos.
  • Tus fetch tienen estrategia: revalidate vs no-store.
  • Secrets nunca viajan al browser (sin NEXT_PUBLIC_).
  • Tienes loading.tsx en rutas sensibles a latencia.
  • Hay un endpoint /api/health (o similar) para monitoreo.
  • next build pasa en CI antes del deploy.

FAQ

1) ¿Next.js reemplaza a mi backend?

No necesariamente. Puede ser BFF o manejar algunas rutas, pero si ya tienes backend (Java, .NET, Node, etc.), úsalo. Next brilla coordinando render, caché y experiencia web.

2) ¿Cuándo conviene ISR vs SSR?

ISR si toleras datos con “atraso” controlado (60s, 5 min). SSR si es personalizado por usuario o necesitas consistencia por request (por ejemplo, dashboard con permisos).

3) ¿App Router o Pages Router?

App Router si vas iniciando o quieres lo moderno (Server Components, layouts anidados). Pages Router si traes legacy grande y no puedes migrar aún. En equipos mixtos, migra por módulos.

4) ¿Por qué mi bundle creció si “Next es rápido”?

Porque marcaste todo como 'use client' o metiste librerías pesadas en componentes client. Aquí no hay magia: todo depende de qué mandas al browser.

5) ¿Vercel es obligatorio?

No. Es el camino fácil para prototipos y productos pequeños/medianos, pero puedes deployar en Node servers, containers o plataformas tipo Railway/Fly. Solo entiende bien caching y runtime para no llevarte sorpresas en production.

Siguiente episodio: teaser

Ya que el servidor regresó, el siguiente nivel es datos, estado y single source of truth sin que tu app se vuelva una telenovela de renders.

La meta: que tu workflow no dependa de rezar antes del deploy.