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,
fetchcon 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 layoutsapp/page.tsx→ homeapp/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
useStateniuseEffect. - 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→/productosapp/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.

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/conpage.tsx,layout.tsx,loading.tsx. - Network tab comparando: SPA vs Next (ver HTML con contenido ya renderizado).
- Ejemplo de
next buildy 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.

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
fetchtienen estrategia:revalidatevsno-store. - Secrets nunca viajan al browser (sin
NEXT_PUBLIC_). - Tienes
loading.tsxen rutas sensibles a latencia. - Hay un endpoint
/api/health(o similar) para monitoreo. -
next buildpasa 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.
Idea para cerrar bien este post: toma una sola práctica de aquí y conviértela en algo que tu equipo pueda aplicar hoy.
Cuando un artículo aterriza en decisiones reales, deja de ser contenido y se vuelve ventaja.


