Next.js
Cómo crear un sitio web con Next.js para producción: App Router, datos, caché y despliegue
Crea un sitio de producción con Next.js, App Router, Server Components, caché explícita, mutaciones seguras, SEO, pruebas y despliegue.
Un sitio de producción con Next.js es más que una colección de componentes React. El enrutamiento, los límites entre servidor y cliente, la vigencia de los datos, los metadatos, los estados de fallo y el contrato de despliegue deben ser coherentes.
1. Decide qué debe hacer el sitio web
Empieza por las responsabilidades del sitio, no por su biblioteca de componentes. Enumera las rutas públicas y privadas, las fuentes de contenido, las mutaciones, los requisitos de búsqueda y los servicios externos. Esto indica qué páginas pueden ser estáticas, cuáles necesitan datos durante la solicitud y dónde debe aplicarse la autorización.
En un sitio típico de producto, las páginas de marketing y documentación cambian relativamente despacio, el blog se genera a partir del contenido y la zona de cuenta depende del usuario que haya iniciado sesión. Son tres vigencias de datos distintas. Tratar todo como una aplicación de página única renderizada en el cliente desaprovecha el renderizado útil del servidor; tratar todo como HTML estático hace imposible la zona de cuenta.
Escribe el mapa de rutas antes de implementar. Asigna a cada página importante una función principal y una URL canónica. Decide qué entidades necesitan segmentos dinámicos, como /blog/[slug], y qué interfaz debe persistir durante la navegación en un layout compartido.
- —Contenido público: inicio, producto, precios, información, blog, guías y páginas legales.
- —Contenido de la aplicación: tablero, ajustes, facturación u otras rutas dependientes de la sesión.
- —Límites de datos: archivos locales, un CMS, una base de datos, API externas y entrada de usuario.
- —Necesidades operativas: variables de entorno, comprobaciones de salud, registros, trabajo programado y despliegue.
2. Crea el proyecto con valores predeterminados elegidos expresamente
Esta guía usa App Router de Next.js 16, TypeScript y un directorio src. Los valores predeterminados actuales de create-next-app son razonables para un proyecto nuevo, pero las opciones explícitas permiten que los compañeros y CI reproduzcan la configuración.
Ejecuta el servidor de desarrollo y haz inmediatamente una compilación de producción. Compilar el primer día detecta problemas de versión de Node.js, importaciones incompatibles y errores de configuración antes de que el proyecto crezca a su alrededor. Conserva el archivo de bloqueo de npm en el control de versiones y usa npm ci en compilaciones automáticas para mantener reproducible la resolución de dependencias.
Terminal
npx create-next-app@latest northstar \
--ts --tailwind --eslint --app --src-dir \
--import-alias "@/*"
cd northstar
npm run dev
npm run build3. Organiza las rutas según los recorridos del usuario
App Router convierte carpetas en segmentos URL. page.tsx hace pública una ruta; layout.tsx envuelve ese segmento y sus descendientes; loading.tsx ofrece un estado provisional durante la transmisión; error.tsx captura errores de renderizado no controlados; y not-found.tsx gestiona recursos ausentes. Coloca los archivos cerca de la ruta a la que pertenecen, en lugar de crear una carpeta global de componentes sin límites claros.
Los grupos de rutas, como (marketing) y (app), organizan carpetas sin cambiar la URL. Ayudan cuando el sitio público y el producto autenticado necesitan layouts distintos. Los segmentos dinámicos, como [slug], reciben parámetros de ruta; los segmentos catch-all, como [...parts], capturan varios niveles.
Usa rutas paralelas avanzadas e interceptadas solo cuando las necesite la interacción. Una página de foto compartible que se abra como modal durante la navegación interna encaja bien. Una página normal de ajustes no. El árbol de rutas más simple que corresponda al modelo mental del usuario suele ser el más fácil de depurar.
A practical App Router structure
src/app/
├── layout.tsx
├── globals.css
├── (marketing)/
│ ├── layout.tsx
│ ├── page.tsx
│ ├── pricing/page.tsx
│ └── blog/
│ ├── page.tsx
│ └── [slug]/page.tsx
├── (app)/
│ ├── dashboard/page.tsx
│ ├── settings/page.tsx
│ └── loading.tsx
├── api/health/route.ts
├── not-found.tsx
└── global-error.tsx4. Mantén el layout raíz pequeño y estable
El layout raíz es obligatorio y contiene los elementos html y body. Coloca allí lo verdaderamente global: el idioma del documento, las variables de fuentes de todo el sitio, CSS global, un proveedor de tema cuando sea necesario y metadatos predeterminados compartidos. No lo conviertas en un contenedor de consultas específicas de rutas ni de un gran árbol de proveedores de cliente.
Los layouts persisten durante la navegación del cliente, por lo que son adecuados para la navegación estable y la estructura de la página. Un archivo template.tsx se comporta de otra forma: recibe una clave nueva y vuelve a montarse cuando cambia su segmento. Elige una plantilla solo cuando quieras reiniciar al navegar, por ejemplo una animación de entrada o el estado local.
src/app/layout.tsx
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"], display: "swap" });
export const metadata: Metadata = {
metadataBase: new URL("https://example.com"),
title: { default: "Northstar", template: "%s | Northstar" },
description: "Planning software for focused product teams.",
};
export default function RootLayout({
children,
}: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}5. Usa Server Components de forma predeterminada
Las páginas y layouts son Server Components salvo que marques un módulo con la directiva use client. Pueden consultar una base de datos, leer variables de entorno exclusivas del servidor, llamar a servicios internos y enviar una salida renderizada sin añadir el código del componente al paquete del navegador.
Añade un Client Component en el límite interactivo útil más pequeño. Un filtro puede necesitar estado, controladores de eventos y APIs de URL; la cuadrícula de productos de debajo puede seguir siendo un Server Component. Marcar toda la página como Client Component traslada al navegador más JavaScript y responsabilidad de obtención de datos de lo que necesita la interacción.
Las props que pasan del servidor al cliente deben ser serializables. Pasa un modelo de vista limitado en lugar de un registro de base de datos con campos privados. Importa server-only en los módulos de datos que nunca deban entrar en un paquete cliente; así, una vulneración accidental del límite se convierte en un error de compilación.
A small client island inside a server-rendered page
// src/app/products/page.tsx — Server Component
import { getProducts } from "@/lib/data";
import ProductFilters from "./product-filters";
export default async function ProductsPage() {
const products = await getProducts();
return <ProductFilters products={products} />;
}
// src/app/products/product-filters.tsx — Client Component
"use client";
import { useState } from "react";
export default function ProductFilters({ products }) {
const [query, setQuery] = useState("");
const visible = products.filter((product) =>
product.name.toLowerCase().includes(query.toLowerCase()),
);
return <>{/* input and product list */}</>;
}6. Obtén los datos donde se renderizan
Un Server Component asíncrono puede llamar directamente a fetch, un ORM o un cliente de base de datos. Esto evita crear un endpoint HTTP interno solo para que el código renderizado en el servidor se llame a sí mismo. Mantén la consulta en un módulo de acceso a datos exclusivo del servidor cuando la usen varias rutas o cuando la autorización y la definición de la salida deban concentrarse en un lugar auditado.
Evita las cascadas de solicitudes. Si dos consultas son independientes, inícialas juntas con Promise.all. Si solo un componente hijo necesita datos más lentos, deja que los obtenga y coloca un límite Suspense a su alrededor. La página puede enviar HTML útil mientras termina la sección más lenta.
Decide qué vigencia necesita cada resultado antes de añadir una caché. Los datos de cuenta específicos del usuario suelen consultarse durante la solicitud. Una tabla pública de precios puede guardarse en caché. Un catálogo de productos puede usar una caché etiquetada que se invalide cuando un editor publique un cambio.
Parallel server-side data fetching
import "server-only";
export default async function DashboardPage() {
const [account, activity] = await Promise.all([
getAccount(),
getRecentActivity(),
]);
return <Dashboard account={account} activity={activity} />;
}7. Trata la caché como una decisión de producto
Next.js 16 permite activar Cache Components de forma opcional. Con cacheComponents habilitado, la directiva use cache puede guardar en caché una función o componente asíncrono. cacheLife define su vigencia, cacheTag asigna una clave compartida de invalidación a entradas relacionadas y un límite Suspense transmite el trabajo de la solicitud sin caché junto a la estructura en caché.
Guardar contenido en caché no es automáticamente correcto porque sea público. Pregunta qué sucede cuando el resultado queda obsoleto, cómo se invalida, si la plataforma de despliegue comparte la caché y si algún valor de sesión puede entrar en su clave. Nunca guardes el resultado privado de un usuario con una clave que pueda recibir otro.
Usa updateTag desde una Server Action cuando el mismo usuario deba ver la mutación inmediatamente. Usa revalidateTag con un perfil de caché adecuado cuando sea aceptable stale-while-revalidate, o revalidatePath cuando el límite de invalidación sea una página o layout. Mantén la invalidación junto a la mutación que deja obsoletos los datos en caché.
next.config.ts and src/lib/products.ts
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = { cacheComponents: true };
export default nextConfig;
// src/lib/products.ts
import "server-only";
import { cacheLife, cacheTag } from "next/cache";
export async function getProducts() {
"use cache";
cacheLife("hours");
cacheTag("products");
return db.product.findMany();
}8. Diseña los estados de carga, vacío, error y recurso ausente
El funcionamiento correcto es solo un estado. loading.tsx crea un límite Suspense de ruta y permite que la navegación muestre de inmediato un estado provisional. Añade límites Suspense más pequeños cuando las regiones independientes deban mostrarse por separado. Un esqueleto útil conserva el layout final en lugar de sustituir la página por un indicador que provoque un gran desplazamiento.
Los fallos esperados pertenecen al flujo de control normal. Un error de validación debe devolver un resultado tipado al formulario. Un registro ausente debe llamar a notFound. Un fallo de permisos debe producir la respuesta o redirección adecuada. Reserva error.tsx para las excepciones no capturadas, registra el error con suficiente contexto de solicitud y versión para investigarlo, y ofrece al usuario una acción segura de recuperación.
Los resultados vacíos no son errores. Una cuenta nueva sin proyectos debe explicar qué contiene la página y cómo crear el primero. Esa pequeña distinción facilita entender la interfaz y mantiene la supervisión centrada en los fallos reales.
src/app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
export default async function PostPage({ params }) {
const { slug } = await params;
const post = await getPost(slug);
if (!post) notFound();
return <article>{/* post content */}</article>;
}9. Gestiona explícitamente las mutaciones y los endpoints HTTP
Las Server Actions encajan con las mutaciones activadas por tu interfaz React. Marca la función con use server, valida FormData en el servidor, verifica la sesión y los permisos del usuario, escribe los cambios e invalida la caché afectada. Trata cada Server Action exportada como un endpoint público: ocultar el botón no equivale a autorizar.
Usa Route Handlers para interfaces HTTP que vayan más allá de un formulario React, como webhooks, comprobaciones de salud, respuestas de archivos y endpoints de aplicaciones móviles o terceros. Exporta los handlers de los verbos HTTP necesarios y devuelve objetos Web Response estándar. Valida las firmas antes de analizar campos fiables del webhook y haz idempotentes las operaciones que se reintenten.
Mantén los efectos secundarios fuera del renderizado. Enviar correo, cobrar una tarjeta o escribir datos analíticos desde el cuerpo de un componente puede ocurrir más de una vez. Realiza estas acciones en un límite de mutación, registra suficiente estado para reintentarlas con seguridad y traslada el trabajo largo a una cola cuando la solicitud no deba esperarlo.
src/app/projects/actions.ts
"use server";
import { updateTag } from "next/cache";
export async function createProject(formData: FormData) {
const session = await verifySession();
if (!session) throw new Error("Unauthorized");
const input = projectSchema.parse({
name: formData.get("name"),
});
await db.project.create({ data: { ...input, ownerId: session.userId } });
updateTag("projects");
}10. Integra metadatos de búsqueda y de contenido compartido en cada ruta
La optimización de búsqueda empieza con una página útil y rastreable y una URL estable. Da a cada ruta indexable un título específico, una descripción clara, un H1 visible, encabezados con significado, enlaces descriptivos y contenido que responda completamente a la consulta. Los metadatos no compensan una página pobre ni varias URLs que publican el mismo contenido.
Exporta un objeto metadata estático cuando los valores sean fijos. Usa generateMetadata cuando el título, la descripción, la URL canónica o la imagen dependan de los datos de la ruta. Configura metadataBase una vez en el layout raíz para resolver correctamente las URLs relativas canónicas y de imágenes sociales. Genera el sitemap y el archivo robots desde la misma fuente que define las rutas públicas y excluye de la indexación las URLs privadas o duplicadas.
Añade Article, Product, BreadcrumbList u otro tipo JSON-LD adecuado solo cuando lo respalde la página visible. Los datos estructurados deben describir lo que el lector realmente puede ver. Valídalos tras el renderizado y actualiza dateModified cuando cambie sustancialmente el contenido.
- —Usa app/robots.ts y app/sitemap.ts para los archivos generados de rastreadores.
- —Añade archivos opengraph-image y twitter-image cuando una ruta necesite imágenes sociales generadas.
- —Redirige las URLs retiradas y elige un host y un protocolo canónicos y una política para la barra final.
- —Enlaza las páginas relacionadas con texto descriptivo para que puedan descubrirlas los usuarios y los rastreadores.
Metadata for a dynamic article
import type { Metadata } from "next";
export async function generateMetadata({ params }): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
if (!post) return { title: "Article not found" };
return {
title: post.title,
description: post.excerpt,
alternates: { canonical: "/blog/" + post.slug },
openGraph: {
type: "article",
title: post.title,
description: post.excerpt,
images: [post.ogImage],
},
};
}11. Protege el rendimiento en el límite de cada componente
Next.js ofrece división del código por rutas, Server Components, precarga, optimización de imágenes y herramientas de fuentes, pero las decisiones de la aplicación siguen determinando el resultado. Vigila el límite del cliente: una directiva use client incorpora ese módulo y sus importaciones de cliente al grafo del navegador. Las bibliotecas grandes de gráficos, editores, mapas y analítica requieren estrategias de carga deliberadas.
Usa next/image con dimensiones reales o un contenedor fill controlado para que el navegador reserve espacio. Usa next/font para alojar y precargar las fuentes que necesita realmente tu diseño. Carga scripts de terceros con next/script y la estrategia menos agresiva que cumpla los requisitos del negocio.
Mide el comportamiento en producción, además del servidor de desarrollo. Ejecuta Lighthouse como comprobación de laboratorio, recoge Core Web Vitals de visitas reales, inspecciona las consultas lentas del servidor y analiza el paquete del cliente cuando una ruta empeore. Los límites de rendimiento son más útiles cuando identifican una ruta y una métrica, en lugar de una única puntuación para todo el sitio.
A responsive image with reserved space
import Image from "next/image";
<Image
src="/product-dashboard.png"
alt="Northstar dashboard showing the weekly plan"
width={1600}
height={900}
sizes="(max-width: 768px) 100vw, 800px"
priority
/>12. Coloca la autenticación junto al acceso a datos
La autenticación demuestra la identidad; la autorización decide qué puede hacer. Usa una biblioteca de autenticación mantenida, salvo que el producto tenga una buena razón para gestionar contraseñas, rotación de sesiones, recuperación de cuentas e integración con proveedores. Guarda el material de sesión en cookies seguras HTTP-only y mantén las lecturas sensibles en el servidor.
Centraliza la autorización segura en una capa de acceso a datos y compruébala de nuevo en cada Server Action y Route Handler. Proxy puede hacer redirecciones preliminares cerca del límite de la ruta, pero no es la única protección: los usuarios pueden llamar directamente a endpoints públicos de mutación, y el código de servidor anidado necesita sus propias comprobaciones.
Solo las variables de entorno con prefijo NEXT_PUBLIC_ pertenecen al código del navegador. Considera públicos esos valores durante la compilación. Marca los módulos de datos privados con server-only, devuelve DTO limitados a los Client Components, valida cada entrada y escapa o sanea el contenido enriquecido no fiable antes de renderizarlo.
- —Usa cookies seguras, HTTP-only y same-site para sesiones cuando lo permita el diseño de autenticación.
- —Comprueba la propiedad o el rol en cada lectura y escritura sensible.
- —Verifica las firmas webhook con el cuerpo original de la solicitud cuando lo requiera el proveedor.
- —Añade una Content Security Policy que corresponda a los scripts y recursos que realmente carga el sitio.
- —Nunca incluyas secretos, registros privados de base de datos ni detalles de error sin limitar en las props de un Client Component.
13. Prueba el comportamiento en el nivel adecuado
Haz pruebas unitarias de reglas de negocio puras sin involucrar Next.js. Prueba por integración la capa de datos, las Server Actions y los Route Handlers con límites realistas. Usa pruebas de navegador para los pocos recorridos cuyo fallo bloquearía al usuario: iniciar sesión, crear el recurso principal, completar el pago o publicar contenido.
La accesibilidad debe formar parte de la implementación y la revisión, no limitarse a un análisis final. Usa elementos semánticos, foco de teclado visible, etiquetas de formulario asociadas, mensajes de error útiles, contraste suficiente y comportamiento de movimiento reducido. Las comprobaciones automáticas detectan una parte; revisar con teclado y lector de pantalla revela problemas que el árbol de componentes no puede mostrar por sí solo.
Incluye la compilación de producción en la integración continua. Next.js 16 ya no usa next build para ejecutar el linter; ejecuta el lint, la comprobación de tipos, las pruebas y la compilación de producción mediante comandos explícitos. Inicia la aplicación compilada y prueba su salud y rutas críticas antes de promoverla.
A straightforward CI verification sequence
npm ci
npm run lint
npx tsc --noEmit
npm test
npm run build
npm start14. Despliega el proceso que probaste
Una aplicación Next.js renderizada en el servidor necesita un entorno de ejecución Node.js compatible, su salida de compilación, valores de entorno, un comando de inicio y una comprobación de salud. Compila desde una copia limpia del repositorio con el archivo de bloqueo guardado en commits. Mantén los valores privados en el almacén de secretos de la plataforma de despliegue, fuera del repositorio y de la imagen.
Una ruta de salud debe indicar si esta versión puede recibir tráfico. Mantén bajo su coste y evita devolver secretos o detalles internos. Inspecciona por separado los registros de compilación y del entorno de ejecución, verifica la ruta generada y conecta después el dominio personalizado y TLS gestionado. Si la aplicación depende del disco local, tareas en segundo plano, optimización de imágenes o Cache Components, confirma que el entorno de alojamiento admite el comportamiento elegido.
Adios ejecuta el servidor normal de producción de Next.js como un proceso persistente de Node.js. El manifiesto de despliegue conserva el contrato de compilación, inicio, puerto, entorno de ejecución y salud junto al código fuente para revisar los mismos supuestos antes de publicar una versión.
adios.yaml
name: northstar
region: de
replicas: 1
build_cmd: npm ci && npm run build
start_cmd: npm start
runtime:
name: node@24
port: 3000
health_path: /api/health
memory_mb: 1024
env:
DATABASE_URL: secret://DATABASE_URL
AUTH_SECRET: secret://AUTH_SECRET15. Usa una lista de comprobación de publicación
La revisión final debe conectar el comportamiento del producto con el del entorno de ejecución. Prueba una compilación limpia, visitas directas a cada ruta crítica, navegación de cliente entre layouts, una dependencia lenta, un error de validación esperado, una excepción no capturada, un registro ausente y una solicitud no autorizada. Consulta el código HTML de las páginas públicas para confirmar que el contenido y los metadatos importantes aparecen sin esperar al JavaScript del cliente.
Después prueba la recuperación. Detén una dependencia necesaria, envía la misma mutación dos veces, rota un secreto y despliega una versión que falle su comprobación de salud. Un sitio está listo cuando el equipo puede explicar cómo empieza, cómo falla, cómo se protege a los usuarios durante el fallo y cómo sigue disponible la última versión saludable.
- —Rutas: URL canónicas, redirecciones, comportamiento 404, mapa de sitio y reglas de robots son correctas.
- —Renderizado: los límites servidor/cliente son deliberados y las secciones lentas transmiten estados provisionales útiles.
- —Datos: caché, invalidación, autorización, estados vacíos y estados de error coinciden con el producto.
- —Calidad: pasan el lint, los tipos, las pruebas, la accesibilidad, la compilación de producción y las pruebas básicas.
- —Operaciones: están documentados los secretos, la salud, los registros, el dominio, TLS, la reversión y la propiedad.