Next.js
Server Components en Next.js
Explica cómo los Server Components cargan datos, usan secretos, reducen JavaScript cliente y se combinan con caché, streaming y DTOs seguros.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
Next.js
Explica cómo los Server Components cargan datos, usan secretos, reducen JavaScript cliente y se combinan con caché, streaming y DTOs seguros.
En el App Router, pages y layouts son Server Components por defecto. Su implementación se ejecuta en el entorno servidor y no se descarga como JavaScript cliente. Esto permite cargar datos cerca de la interfaz y reducir bundles, pero obliga a comprender serialización, caché y fronteras de seguridad.
Un Server Component es un componente React cuya implementación pertenece al grafo servidor:
// app/products/page.tsx
export default async function ProductsPage() {
const products = await productRepository.listPublished();
return (
<main>
<h1>Productos</h1>
<ProductList products={products} />
</main>
);
}Puede ser async, consultar una base de datos y utilizar secretos del entorno servidor. Su código no necesita enviarse al navegador para representar el resultado.
Next.js procesa el componente y genera una representación React Server Components, además de HTML inicial cuando corresponde.
En una SPA tradicional, el navegador puede necesitar:
descargar JavaScript de la page
↓
ejecutar componente
↓
solicitar API
↓
esperar JSON
↓
renderizar contenidoEsto puede crear:
Con Server Components:
servidor carga datos
↓
compone árbol React
↓
HTML + RSC payload
↓
navegador muestra contenidoEl acceso a datos ocurre antes de entregar la representación, sin exponer la implementación privada.
Server Component source
│
├─ puede importar módulos server-only
├─ puede leer datos y secretos
├─ no usa state ni Effects de navegador
↓
React Server Component render
↓
RSC payload
├─ resultado de Server Components
├─ props serializadas
└─ referencias a Client Components
↓
HTML inicial + navegación clienteEl RSC payload no es HTML ni un endpoint JSON convencional. Es una representación especializada que React utiliza para combinar el árbol servidor con las boundaries cliente.
Dentro de app, un módulo es servidor mientras no se encuentre debajo de una boundary "use client":
app/page.tsx → servidor
app/product-list.tsx → servidor si lo importa una rama servidor
app/add-to-cart-button.tsx → cliente si declara use clientNo necesitas escribir "use server" en un componente. Esa directiva tiene otro propósito: marcar Server Functions.
import "server-only";
import { db } from "@/server/db";
export async function getOrder(orderId: string) {
return db.order.findUnique({ where: { id: orderId } });
}La page puede usar el repositorio sin crear un endpoint interno solo para sí misma.
const token = process.env.PAYMENTS_PRIVATE_TOKEN;El valor permanece en servidor mientras no lo serialices ni lo pases a cliente.
Parsers, SDKs administrativos y acceso a filesystem pueden permanecer fuera del bundle cliente si el runtime los soporta.
export default async function InvoicePage({
params,
}: PageProps<"/invoices/[invoiceId]">) {
const { invoiceId } = await params;
const invoice = await getInvoice(invoiceId);
return <InvoiceDocument invoice={invoice} />;
}El componente puede esperar datos durante render servidor y participar en Suspense/streaming.
No pueden usar APIs que dependen de una instancia interactiva en navegador:
useState.useReducer para state local interactivo.useEffect y useLayoutEffect.onClick.window, document, localStorage.Este código no es válido:
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}La solución no es convertir toda la page en cliente. Extrae la región interactiva.
Responden:
¿Dónde se ejecuta la implementación del componente y viaja su código al cliente?
Responde:
¿Se genera HTML inicial en el servidor para una respuesta?
Un Client Component puede participar en SSR y después hidratarse. Un Server Component no se hidrata como una instancia cliente porque su implementación no se descarga.
RSC
→ frontera de módulos y ejecución
SSR
→ generación de HTML inicialUn Server Component puede ejecutarse:
La estrategia depende de datos, Dynamic APIs, caché y configuración.
export default async function AccountPage() {
const session = await auth();
return <Account user={session.user} />;
}Sigue siendo Server Component, aunque dependa de cada request.
En una carga inicial, Next.js coordina:
Server Components
↓
RSC payload
↓
HTML para contenido inicial
↓
JavaScript de Client Components
↓
hidratación de boundaries interactivasEl usuario puede ver contenido producido por Server Components sin descargar su lógica.
En navegación cliente, Next.js puede solicitar un RSC payload nuevo y combinarlo con el árbol existente:
<Link> click
↓
RSC request
↓
server tree result
↓
React reconciliation
↓
layouts/state cliente compatibles se preservanNo es necesario enviar un documento HTML completo.
// app/organizations/[organizationId]/orders/[orderId]/page.tsx
import { notFound } from "next/navigation";
import { requireSession } from "@/server/auth";
import { getAccessibleOrder } from "@/features/orders/data";
import { CancelOrderButton } from "./cancel-order-button";
export default async function OrderPage({
params,
}: PageProps<"/organizations/[organizationId]/orders/[orderId]">) {
const { organizationId, orderId } = await params;
const session = await requireSession();
const order = await getAccessibleOrder({
organizationId,
orderId,
userId: session.user.id,
});
if (!order) {
notFound();
}
return (
<main>
<h1>Pedido {order.number}</h1>
<OrderSummary order={toOrderView(order)} />
<CancelOrderButton orderId={order.id} />
</main>
);
}toOrderView limita los campos enviados.No pases una entidad completa por comodidad:
<OrderClient order={databaseOrder} />Puede incluir:
Crea un DTO explícito:
type OrderView = {
id: string;
number: string;
status: "pending" | "confirmed" | "cancelled";
total: number;
};
function toOrderView(order: OrderRecord): OrderView {
return {
id: order.id,
number: order.number,
status: order.status,
total: order.total,
};
}El hecho de que el componente sea servidor protege la implementación, no los valores que terminas enviando.
Los datos que cruzan a una Client Component deben pertenecer al contrato serializable de React/Next.js.
Prefiere:
Evita cruzar:
Aunque React soporte ciertos tipos adicionales, un DTO simple mejora compatibilidad y comprensión.
import "server-only";Añádelo a módulos que nunca deben importarse desde una boundary cliente:
// src/server/db.ts
import "server-only";
export const db = createDatabaseClient(process.env.DATABASE_URL!);Si un Client Component lo importa accidentalmente, Next.js produce un error de build útil.
No sustituye una revisión de datos enviados.
React y Next.js pueden ofrecer tainting bajo configuración compatible para marcar objetos o valores que no deben cruzar. Es defensa adicional, no una política completa.
La defensa en profundidad incluye:
server-only modules
+ DTOs mínimos
+ autorización
+ validación
+ revisión de serialización
+ observabilidad sin secretosUn Server Component puede usar:
const response = await fetch("https://api.example.com/products");Next.js puede extender su comportamiento de caché y revalidation según versión/configuración.
const products = await db.product.findMany();Las consultas directas no reciben automáticamente el mismo modelo de caché de fetch. Usa cache, use cache o una capa propia cuando quieras reutilización.
const customer = await billingAdmin.customers.get(id);Mantén el SDK y token en servidor.
React cache puede memoizar una función durante un render/request bajo su contrato:
import { cache } from "react";
export const getUser = cache(async (userId: string) => {
return userRepository.findById(userId);
});Esto ayuda cuando metadata, layout y page necesitan la misma entidad.
No lo confundas con una caché persistente entre requests.
export default function ProductPage() {
return (
<main>
<ProductHeader />
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations />
</Suspense>
</main>
);
}El Server Component Recommendations puede esperar datos. El servidor transmite primero la shell y completa la región después.
No necesitas mover el fetch a useEffect para mostrar loading.
Secuencial accidental:
const user = await getUser();
const orders = await getOrders(user.id);
const recommendations = await getRecommendations();orders depende de user, pero recommendations no. Iníciala antes:
const recommendationsPromise = getRecommendations();
const user = await getUser();
const [orders, recommendations] = await Promise.all([
getOrders(user.id),
recommendationsPromise,
]);Server Components no eliminan waterfalls automáticamente. La estructura de await importa.
Un error inesperado durante render puede activar error.tsx. Un resultado esperado debe modelarse:
const result = await getProduct(slug);
if (result.status === "notFound") notFound();
if (result.status === "forbidden") forbidden();No envíes el mensaje interno del error servidor a cliente. Usa digest y logging correlacionado.
Un Server Component se ejecuta bajo el runtime de la ruta/plataforma.
Node.js permite la mayoría de SDKs de servidor. Edge u otros entornos tienen restricciones.
La palabra “server” no garantiza que exista filesystem persistente ni procesos largos.
La decisión se toma por capacidad, no por preferencia estética.
Puede mover datos y dependencias al navegador. Identifica la capacidad concreta.
Añade una vuelta HTTP innecesaria cuando el consumidor es solo el servidor Next.js. Mantén endpoints para clientes externos o fronteras reales.
Filtra datos y aumenta payload.
Una consulta directa puede ejecutarse repetidamente. Diseña la capa de caché explícita.
Usa server-only y boundaries pequeñas.
Puede ser prerenderizado.
Render debe ser idempotente. Las mutaciones pertenecen a Actions, Route Handlers o jobs.
Prueba repositorios y autorización sin montar la page.
Invoca la función bajo un entorno controlado o usa integración del framework.
Verifica HTML inicial, ausencia de secretos, navegación y permisos.
Comprueba que SDKs privados no aparezcan en chunks cliente.
server-only.fetch?server-only?Client Components y la boundary use client explica qué código cruza al navegador y cómo localizar la interactividad sin convertir todo el árbol en cliente.