Next.js
Data fetching en Server Components
Explica cómo leer APIs, bases de datos y SDKs desde Server Components, evitando HTTP interno, waterfalls, fugas de datos y cachés mal aisladas.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
Next.js
Explica cómo leer APIs, bases de datos y SDKs desde Server Components, evitando HTTP interno, waterfalls, fugas de datos y cachés mal aisladas.
En Server Components puedes leer datos directamente desde fetch, una base de datos o un SDK privado. La decisión importante no es solo “cómo obtenerlos”, sino dónde validar, cómo evitar waterfalls, qué se puede cachear, cómo se maneja un fallo y qué información termina cruzando al cliente.
El App Router acerca la lectura de datos al componente que necesita representarlos:
export default async function OrdersPage() {
const orders = await listAccessibleOrders();
return <OrdersTable orders={orders} />;
}No necesitas crear un endpoint interno para que el mismo servidor se llame por HTTP:
Server Component
→ función de datos
→ DB/API/SDKFrente a:
Server Component
→ HTTP hacia /api propio
→ Route Handler
→ función de datos
→ DBLa segunda ruta añade serialización, red interna, status y manejo duplicado sin aportar una frontera real cuando solo existe un consumidor servidor.
async function getPosts() {
const response = await fetch("https://api.example.com/posts");
if (!response.ok) {
throw new Error(`Posts request failed: ${response.status}`);
}
return postsSchema.parse(await response.json());
}Next.js amplía fetch con integración de caché y logging. Las peticiones idénticas dentro del árbol React se memoizan para evitar duplicados durante trabajo relacionado, pero en el modelo actual no debes asumir que toda respuesta queda cacheada entre requests.
import "server-only";
export async function listOrdersForUser(userId: string) {
return db.order.findMany({
where: { customerId: userId },
orderBy: { createdAt: "desc" },
});
}Las credenciales y query logic permanecen en servidor. La consulta no recibe automáticamente las políticas de fetch; decide caché mediante use cache, React cache o tu capa de datos.
const invoices = await billingAdmin.invoices.list({ customerId });Mantén el SDK y su token fuera del grafo cliente. Valida el resultado aunque provenga de un proveedor confiable, porque el contrato puede cambiar o contener estados no contemplados.
Una page no debería repetir autorización y mapping:
import "server-only";
export async function getAccessibleOrder(input: {
orderId: string;
organizationId: string;
userId: string;
}): Promise<OrderView | null> {
const order = await db.order.findFirst({
where: {
id: input.orderId,
organizationId: input.organizationId,
organization: {
members: { some: { userId: input.userId } },
},
},
});
return order ? toOrderView(order) : null;
}La DAL concentra:
La page sigue coordinando routing y UI.
async function OrderSummary({ orderId }: { orderId: string }) {
const order = await getOrderSummary(orderId);
return <Summary order={order} />;
}Esto evita elevar datos a layouts que no los necesitan. La colocation no significa duplicar consultas: fetch idéntico o una función con React.cache puede deduplicar.
import { cache } from "react";
export const getCurrentUser = cache(async () => {
const session = await requireSession();
return userRepository.findById(session.user.id);
});Múltiples Server Components pueden llamar getCurrentUser() durante el mismo request sin repetir trabajo.
React.cache
→ memoización por request/render
→ no caché persistente entre usuariosNo lo confundas con use cache, que define reutilización más amplia según key y lifetime.
Existe dependencia real:
const organization = await getOrganization(slug);
const projects = await getProjects(organization.id);La segunda consulta necesita el ID de la primera. Suspense puede permitir mostrar otras partes mientras termina, pero no elimina la dependencia.
Optimiza la primera operación, cachea si corresponde o cambia la consulta para obtener ambas en una sola operación.
const profile = await getProfile(userId);
const notifications = await getNotifications(userId);
const preferences = await getPreferences(userId);Son independientes y se ejecutan una tras otra.
const profilePromise = getProfile(userId);
const notificationsPromise = getNotifications(userId);
const preferencesPromise = getPreferences(userId);
const [profile, notifications, preferences] = await Promise.all([
profilePromise,
notificationsPromise,
preferencesPromise,
]);Promise.all falla completo si una operación falla. Usa Promise.allSettled solo cuando la pantalla puede representar resultados parciales y diseña el tipo de cada rama.
Layouts y pages pueden comenzar su trabajo en paralelo. Dentro de un componente, cada await sí puede serializar operaciones.
También puedes separar regiones:
export default function DashboardPage() {
return (
<main>
<Suspense fallback={<ProfileSkeleton />}>
<Profile />
</Suspense>
<Suspense fallback={<NotificationsSkeleton />}>
<Notifications />
</Suspense>
</main>
);
}Cada componente inicia su propia lectura y se transmite cuando termina.
loading.tsx cubre la page/segmento completo. Una consulta en el layout del mismo segmento puede bloquear antes de alcanzar esa boundary.
Para datos runtime o lentos, coloca Suspense cerca del acceso:
<Suspense fallback={<AccountMenuSkeleton />}>
<AccountMenu />
</Suspense>Esto mantiene el resto del layout disponible.
Con Cache Components:
async function LiveInventory({ productId }: { productId: string }) {
return <Stock value={await inventory.get(productId)} />;
}Debe ir bajo Suspense para ejecutarse en request.
async function ProductDescription({ productId }: { productId: string }) {
"use cache";
cacheLife("hours");
cacheTag(`product:${productId}`);
const product = await catalog.get(productId);
return <Description product={product} />;
}La decisión depende de cuánto tiempo toleras obsolescencia y quién puede compartir el resultado.
fetch acepta AbortSignal.timeout en runtimes compatibles:
const response = await fetch(url, {
signal: AbortSignal.timeout(5_000),
});Para SDKs/DB usa sus APIs de timeout. Sin límites, una dependencia lenta puede mantener una región o request abierto hasta el timeout de plataforma.
El timeout no recupera automáticamente. Decide si:
error.tsx.No reintentes indiscriminadamente:
Retry-After.Aplica límites, jitter y observabilidad.
type ProductResult =
| { status: "found"; product: ProductView }
| { status: "notFound" }
| { status: "forbidden" };La page traduce cada resultado:
const result = await findProduct(input);
if (result.status === "notFound") notFound();
if (result.status === "forbidden") forbidden();
return <Product product={result.product} />;Un error de red inesperado se lanza hacia una boundary. No conviertas todo fallo en not found.
Una colección vacía es un resultado válido:
if (orders.length === 0) {
return <EmptyOrdersState />;
}Una entidad inexistente puede ser not found. No uses el mismo componente para ambos si la acción siguiente es distinta.
const result = productApiResponseSchema.safeParse(await response.json());
if (!result.success) {
reportContractError(result.error);
throw new Error("Invalid product API response");
}TypeScript no valida JSON. Un cast as Product[] solo oculta el riesgo.
Determina quién es el usuario.
Comprueba que puede leer la entidad concreta.
Selecciona campos necesarios.
No cachees un resultado privado con una key incompleta.
Evita imports accidentales.
Si la URL de fetch depende de input, usa allowlist de hosts y protocolos. No permitas que un usuario solicite metadata endpoints o red interna.
Úsalo cuando:
Para contenido inicial, el servidor suele evitar el waterfall.
Server Component:
const postsPromise = getPosts();
return (
<Suspense fallback={<PostsSkeleton />}>
<Posts postsPromise={postsPromise} />
</Suspense>
);Client Component:
"use client";
export function Posts({ postsPromise }: { postsPromise: Promise<Post[]> }) {
const posts = use(postsPromise);
return <PostList posts={posts} />;
}La Promise debe ser creada en servidor o una capa estable. Este patrón aporta cuando el cliente necesita consumir el resultado dentro de Context/interaction; no es obligatorio para mostrar una lista servidor.
export default function DashboardPage() {
return (
<main>
<DashboardHeader />
<Suspense fallback={<MetricsSkeleton />}>
<Metrics />
</Suspense>
<Suspense fallback={<OrdersSkeleton />}>
<RecentOrders />
</Suspense>
<Suspense fallback={<AlertsSkeleton />}>
<Alerts />
</Suspense>
</main>
);
}La page no espera todo con un único Promise.all si cada región puede revelarse independientemente.
Pregunta:
Usa logs de fetch, tracing, DB query logs y métricas de duración.
Añade latencia y duplicación.
Bloquea operaciones independientes.
Afecta todas las rutas y puede bloquear loading.tsx.
No valida contratos.
Consume recursos hasta el límite de plataforma.
Filtra campos y aumenta payload.
Puede mezclar usuarios.
Duplica efectos.
getOrder(orderId) en multi-tenant?Modelo de caching en Next.js separa memoización, caché de datos, output renderizado, router cache y HTTP cache.