La escalabilidad a largo plazo de una aplicación frontend corporativa depende de la disciplina arquitectónica: en concreto, del equilibrio entre el Acoplamiento (Coupling) y la Cohesión (Cohesion). En prototipos iniciales, la organización estándar de Next.js (agrupar código libremente en app/ o pages/) es admisible. Sin embargo, al crecer hacia cientos de rutas, decenas de subdominios de negocio, integraciones con APIs externas y gestión de estado distribuida, la falta de barreras arquitectónicas desemboca en dependencias circulares y código espagueti.
Organizar una base de código exclusivamente por tipo técnico de archivo (components/, hooks/, utils/, types/) colapsa a gran escala: el desarrollador se ve forzado a explorar decenas de carpetas independientes para modificar un único formulario de compra. El estándar de ingeniería moderno exige el uso de cortes verticales (Vertical Slices) y metodologías como Feature-Sliced Design (FSD). La lógica de negocio, la interfaz de usuario, los esquemas de validación y las mutaciones de datos se agrupan en torno a modelos de dominio, aislando los cambios y eliminando riesgos de regresión.
En Next.js App Router, el diseño arquitectónico se complejiza por el modelo de ejecución híbrido: el árbol de componentes se bifurca entre el entorno de servidor (React Server Components, Node.js/Edge) y el de cliente (DOM/Hydration). La arquitectura debe asegurar que secretos privados, consultas ORM y módulos pesados de servidor jamás se filtren al bundle de cliente mediante reexportaciones accidentales.
Una arquitectura escalable en Next.js se fundamenta en un bajo acoplamiento y una alta cohesión. Cada corte vertical (feature slice) encapsula su ciclo completo: componentes de UI aislados, capa de mutación de datos (Server Actions), tipado estricto y estado local.
En la arquitectura FSD rige una regla cardinal: el Flujo de Dependencias Unidireccional (Unidirectional Dependency Flow). Las capas superiores conocen a las inferiores, pero las inferiores están completamente aisladas del contexto superior.
┌─────────────────────────────────────────────────────────┐
│ NEXT.JS ROUTER │
│ src/app/[slug]/page.tsx │
│ (Enrutamiento, await params, composición raíz) │
└────────────────────────────┬────────────────────────────┘
│ imports ↓
┌────────────────────────────▼────────────────────────────┐
│ WIDGETS │
│ src/widgets/Header/ │
│ (Bloques de UI compuestos y autónomos) │
└──────────────┬───────────────────────────┬──────────────┘
│ imports │ imports
┌──────────────▼──────────┐ ┌──────────────▼──────────────┐
│ FEATURES │ │ FEATURES │
│ src/features/auth/ │ │ src/features/checkout/ │
│(Interacciones de usuario│ │ (Server Actions, formularios│
│ validación Zod) │ └──────────────┬──────────────┘
└──────────────┬──────────┘ │
PROHIBIDO │ × Importaciones cruzadas │
entre peers │ entre features vetadas │ imports
▼ ▼
┌─────────────────────────────────────────────────────────┐
│ ENTITIES │
│ src/entities/user/ src/entities/cart/ │
│ (Entidades de dominio, esquemas, tipos y selectores) │
└────────────────────────────┬────────────────────────────┘
│ imports ↓
┌────────────────────────────▼────────────────────────────┐
│ SHARED │
│ src/shared/ui (UI-Kit) src/shared/lib (Helpers) │
│ src/shared/api (Clientes) src/shared/types (Base) │
└─────────────────────────────────────────────────────────┘Identificar entidades de negocio clave (User, Order) y flujos interactivos de usuario (Auth, Cart, Filter).
Extraer UI-Kit básico, configuraciones tipadas de entorno y utilidades puras hacia src/shared/.
Consolidar esquemas de datos, contratos de base de datos y tarjetas pasivas en src/entities/.
Empaquetar formularios interactivos, Server Actions y esquemas Zod en src/features/.
Integrar features y entidades en bloques autónomos de página (Header, Sidebar, ActivityFeed).
Reducir app/[route]/page.tsx a orquestadores ligeros que esperan parámetros y montan widgets.
En una aplicación de Next.js a gran escala, un desarrollador en la feature de pago (@/features/checkout) importa directamente useCartStore de una feature vecina (@/features/cart). ¿Qué conflicto arquitectónico provoca esto y cómo debe solucionarse según FSD?
Colocation (Coubicación) es la base de un código mantenible. Si un componente o utilidad solo se emplea dentro de una ruta o funcionalidad específica, debe mantenerse localizado en esa porción sin ascenderlo al ámbito global.
tsconfig.json#Los alias de rutas formalizan las capas de la arquitectura a nivel del compilador de TypeScript, erradicando rutas relativas frágiles como ../../../../shared/ui:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@/app/*": ["./src/app/*"],
"@/widgets/*": ["./src/widgets/*"],
"@/features/*": ["./src/features/*"],
"@/entities/*": ["./src/entities/*"],
"@/shared/*": ["./src/shared/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}server-only#Para impedir que clientes de bases de datos, claves secretas y módulos nativos de Node.js se filtren al bundle de cliente, establece barreras estrictas mediante el paquete server-only:
pnpm add server-onlyimport 'server-only';
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const db =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = db;
}Si un módulo con la instrucción import 'server-only' es importado por error en un Client Component con 'use client', Next.js detiene de inmediato la compilación con un mensaje diagnóstico.
Topología de producción probada en grandes plataformas, estructurada dentro de src/:
src/
├── app/ # Next.js App Router: pegamento y rutas
│ ├── (auth)/ # Route Group (sin impacto en la URL pública)
│ │ ├── login/page.tsx
│ │ └── register/page.tsx
│ ├── (dashboard)/
│ │ ├── projects/
│ │ │ ├── [id]/page.tsx
│ │ │ └── _components/ # Private Folder: componentes locales de ruta
│ │ │ └── ProjectHeader.tsx
│ │ └── layout.tsx
│ ├── layout.tsx # Layout raíz
│ └── globals.css
├── widgets/ # Bloques compuestos de nivel de página
│ └── Navbar/
│ ├── ui/Navbar.tsx
│ └── index.ts # API pública del widget
├── features/ # Flujos interactivos de negocio
│ ├── auth-by-email/
│ │ ├── ui/LoginForm.tsx # Formulario de cliente con useActionState
│ │ ├── server/actions.ts # Server Actions con validación Zod
│ │ ├── model/schema.ts # Esquemas DTO
│ │ └── index.ts
│ └── project-create/
├── entities/ # Entidades del dominio
│ ├── user/
│ │ ├── ui/UserAvatar.tsx
│ │ ├── model/types.ts
│ │ └── index.ts
│ └── project/
└── shared/ # Base reutilizable
├── api/ # Clientes SDK externos
├── config/ # Variables de entorno tipadas
├── lib/ # Adaptadores de librerías (db, logger)
├── ui/ # UI-Kit puro (Button, Input, Modal)
└── utils/ # Utilidades puras deterministasLas páginas en src/app/ actúan como orquestadores ligeros: resuelven de forma asíncrona los parámetros de ruta, obtienen los datos del dominio y los transmiten a los widgets compuestos.
import { notFound } from 'next/navigation';
import { ProjectHeader } from './_components/ProjectHeader';
import { fetchProjectById } from '@/entities/project/server';
import { ProjectMembersWidget } from '@/widgets/ProjectMembersWidget';
interface ProjectPageProps {
params: Promise<{ id: string }>;
searchParams: Promise<{ tab?: string }>;
}
export default async function ProjectPage({ params, searchParams }: ProjectPageProps) {
const { id } = await params;
const { tab = 'overview' } = await searchParams;
const project = await fetchProjectById(id);
if (!project) {
notFound();
}
return (
<div className="flex flex-col gap-8 p-6 max-w-7xl mx-auto w-full">
<ProjectHeader project={project} currentTab={tab} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
{/* Contenido dinámico específico de la pestaña */}
</div>
<div>
<ProjectMembersWidget projectId={project.id} />
</div>
</div>
</div>
);
}La indefinición de responsabilidades perjudica la arquitectura. En Fullstack Agentic Lab se aplica la siguiente clasificación:
shared/utils/ — Funciones puras y deterministas:
formatCurrency(amount, currency)).shared/lib/ — Adaptadores de librerías externas:
shared/services/ (o services de un slice) — Capa de integración:
Un equipo crea un archivo src/features/auth/index.ts que reexporta simultáneamente una Server Action (loginAction con acceso a base de datos) y un Client Component (LoginForm con 'use client'). ¿Qué ocurre cuando un componente de cliente en el panel de control importa LoginForm desde dicho barrel file?
Crear un archivo index.ts general que reexporta Server Actions junto con formularios de cliente anula los límites del bundle. Si un componente de cliente importa un elemento de ese barrel, el compilador puede introducir dependencias de servidor en el navegador.
Solución: Segmenta los puntos de entrada según su entorno de ejecución:
@/features/auth/ui — Componentes de interfaz de usuario de cliente.@/features/auth/server — Server Actions y consultas a la base de datos con import 'server-only'.@/features/auth/model — Esquemas de validación Zod y tipos TypeScript puros.Importar componentes o hooks desde @/features/cart directamente dentro de @/features/checkout genera acoplamiento circular. Modificar el carrito inevitablemente compromete el flujo de checkout.
Solución: Si checkout precisa el estado del carrito:
@/widgets/checkout-cart) o en la página del servidor (app/checkout/page.tsx).@/entities/cart.Envolver todo el árbol de RootLayout en proveedores de contexto (ThemeProvider, QueryClientProvider) directamente dentro de app/layout.tsx transforma las páginas secundarias en zonas propensas a problemas de hidratación e infla el Initial JS Payload.
Solución: Crea un componente contenedor de cliente @/shared/providers/AppProviders.tsx con 'use client' y pasa los hijos generados en el servidor mediante el patrón de composición estándar: {children}.
Es común crear carpetas como app/(components)/Header.tsx creyendo que quedan ocultas para el enrutador. Los paréntesis (group) suprimen el segmento de la URL, pero no impiden el enrutamiento si se anidan archivos page.tsx.
Solución: Para coubicar componentes locales y helpers sin peligro dentro de app/, utiliza siempre el prefijo de guion bajo: app/dashboard/_components/.