¿Qué es Next.js?
Next.js es un framework de React que transforma una librería de UI en una plataforma de aplicaciones completa. Añade enrutamiento basado en archivos, renderizado en el servidor, obtención de datos (data fetching), endpoints de API, optimización de imágenes y fuentes, y un runtime de servidor. En lugar de elegir y configurar manualmente un router, una capa de datos y un entorno de compilación, obtienes un stack coherente diseñado para trabajar en conjunto.
Está mantenido por Vercel y se ha convertido en la forma predeterminada de construir aplicaciones de React para producción. El modelo actual es el App Router, basado en React Server Components, el cual cambia dónde se ejecuta tu código y cómo fluyen los datos.
El App Router y el enrutamiento basado en archivos
Las rutas provienen del sistema de archivos. Una carpeta representa un segmento de la URL, y un archivo page.tsx la convierte en una ruta.
app/
├── layout.tsx # root layout, wraps everything
├── page.tsx # /
├── about/page.tsx # /about
├── blog/
│ ├── page.tsx # /blog
│ └── [slug]/page.tsx # /blog/:slug
└── dashboard/
├── layout.tsx # layout for /dashboard/*
└── page.tsx # /dashboard
Los segmentos dinámicos utilizan corchetes, y los layouts se anidan para que la UI compartida permanezca montada durante la navegación. Archivos especiales como loading.tsx, error.tsx y not-found.tsx gestionan los estados correspondientes sin necesidad de código adicional.
// app/blog/[slug]/page.tsx
export default async function PostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{post.content}</article>;
}
Componentes de servidor y de cliente
Este es el concepto que define al Next.js moderno. Por defecto, los componentes son server components: se renderizan en el servidor, pueden leer datos y secretos directamente y no añaden nada al bundle del navegador. Los componentes que requieren interactividad optan por ello mediante "use client".
// app/counter.tsx
"use client";
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
Un server component puede importar y renderizar un client component, pero no al revés. La regla de oro: mantén los componentes en el servidor y desplaza "use client" hacia la hoja interactiva más pequeña. De esta forma, el JavaScript que envías al navegador se mantiene reducido.
Obtención de datos
En el App Router, obtienes los datos en los server components utilizando la función estándar fetch, la cual ha sido extendida con opciones de almacenamiento en caché y revalidación.
// app/posts/page.tsx
async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 60 },
});
if (!res.ok) throw new Error("Failed to load posts");
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts();
return <PostList posts={posts} />;
}
Dado que el código se ejecuta en el servidor, no es necesario configurar indicadores de carga (loading spinners) ni existen cascadas (waterfalls) en el cliente. Puedes revalidar los datos basándote en el tiempo, bajo demanda mediante revalidatePath o revalidateTag, o bien desactivar el almacenamiento en caché por completo. Además, las solicitudes fetch se deduplican dentro de un mismo renderizado.
Route handlers y server actions
Para los endpoints de la API, los route handlers exportan funciones que reciben un web Request y devuelven un Response.
// app/api/posts/route.ts
export async function GET() {
const posts = await db.post.findMany();
return Response.json(posts);
}
export async function POST(request: Request) {
const body = await request.json();
const post = await db.post.create({ data: body });
return Response.json(post, { status: 201 });
}
Para las mutaciones desde tu propia UI, los server actions suelen ser más sencillos. Marca una función con "use server" y llámala desde un formulario o un manejador de eventos.
// app/posts/new/page.tsx
import { revalidatePath } from "next/cache";
async function createPost(formData: FormData) {
"use server";
await db.post.create({ data: { title: String(formData.get("title")) } });
revalidatePath("/posts");
}
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" required />
<button type="submit">Create</button>
</form>
);
}
Los server actions gestionan la validación, las escrituras en la base de datos y la revalidación de la caché de forma conjunta, y el formulario funciona incluso antes de que JavaScript se cargue. Eso es mejora progresiva sin trabajo adicional.
Renderizado y almacenamiento en caché
Next.js soporta varias estrategias de renderizado en una misma aplicación:
- Las páginas estáticas se prerenderizan en el momento del build.
- Las páginas renderizadas en el servidor se generan por cada solicitud.
- El streaming envía el HTML en fragmentos para que los datos lentos no bloqueen la estructura principal (shell).
- El renderizado en el cliente gestiona las islas interactivas después de la hidratación.
El almacenamiento en caché tiene varias capas, desde el fetch cache hasta el route cache completo. Comprenderlas es la principal curva de aprendizaje, y los valores por defecto están diseñados para ser rápidos. Cuando sientas que algo está desactualizado, la solución suele ser una llamada de revalidación explícita en lugar de desactivar el almacenamiento en caché en todas partes.
Optimización integrada
Next.js incluye componentes que gestionan las tareas de rendimiento más comunes:
next/imageredimensiona, carga mediante lazy-loading y sirve formatos modernos.next/fontaloja las fuentes localmente y elimina el layout shift.next/scriptcontrola cómo se cargan los scripts de terceros.- Turbopack acelera el desarrollo y las compilaciones de producción.
Estos valores predeterminados significan que una aplicación de Next.js bien construida es rápida sin necesidad de un proyecto de optimización independiente.
Mejores prácticas
- Mantén los componentes en el servidor y usa
"use client"solo donde sea necesario. - Obtén los datos en server components en lugar de hacerlo en effects.
- Utiliza server actions para las mutaciones y revalida explícitamente.
- Coloca rutas, componentes y acceso a datos juntos bajo
app/. - Añade
loading.tsxyerror.tsxpara ofrecer una buena UX en los límites de las rutas. - Prefiere los componentes de imagen y fuentes del framework antes que el manejo manual.
- Comprende el funcionamiento del caching antes de desactivarlo; opta por una revalidación dirigida.
Errores comunes
- Esparcir
"use client"en la parte superior del árbol y perder los beneficios del servidor. - Hacer fetch en
useEffectcuando un server component sería suficiente. - Olvidar revalidar después de una mutación y mostrar datos obsoletos.
- Asumir que las API de App Router y Pages Router son intercambiables.
- Bloquear una ruta debido a datos lentos en lugar de usar streaming con Suspense.
- Tratar a Next.js solo como una herramienta de front-end y reconstruir un backend independiente.
Próximos pasos
Next.js es la forma más completa de desplegar React hoy en día. Refuerza tus bases de React y TypeScript, compara su modelo con Remix y Astro, y aprende sobre el runtime de Node.js donde se despliega. Después, construye una pequeña aplicación full-stack que incluya un server component, un route handler y una server action.