Next.js
Estructura y arquitectura del proyecto
Explica cómo separar árbol de rutas, features, infraestructura servidor y UI compartida mediante boundaries claras, public APIs y módulos mantenibles.
- Última actualización
- Actualizada
- Nivel
- Aplicación
Next.js
Explica cómo separar árbol de rutas, features, infraestructura servidor y UI compartida mediante boundaries claras, public APIs y módulos mantenibles.
La estructura de un proyecto Next.js debe hacer visibles dos mapas distintos: el árbol de rutas y las responsabilidades del dominio. app describe URLs, layouts y boundaries; las features describen reglas, datos y casos de uso. Hacerlos idénticos suele mezclar transporte, UI y negocio dentro de cada page.tsx.
app/ → routing and composition
features/ → domain capabilities
shared/ui/ → reusable visual primitives
server/ → infrastructure and cross-cutting server codeLas dependencias deberían apuntar hacia módulos estables y no cruzar server/client accidentalmente.
src/
├─ app/
│ ├─ (marketing)/
│ ├─ dashboard/
│ └─ api/
├─ features/
│ └─ orders/
│ ├─ data/
│ ├─ services/
│ ├─ actions/
│ ├─ schemas/
│ ├─ ui/
│ └─ index.ts
├─ shared/
│ ├─ ui/
│ ├─ validation/
│ └─ types/
└─ server/
├─ auth/
├─ db/
├─ env/
└─ observability/No es plantilla obligatoria. Ajusta según tamaño y ownership.
Una page debería componer:
export default async function OrdersPage({ searchParams }: PageProps<"/orders">) {
const filters = parseOrderFilters(await searchParams);
const orders = await listOrdersForCurrentUser(filters);
return <OrdersScreen orders={orders} filters={filters} />;
}Evita colocar allí SQL, parsing duplicado, emails y reglas de autorización. Mantén page, layout, loading, error y metadata cerca porque pertenecen a routing.
Una feature agrupa comportamiento que cambia junto:
orders
├─ list orders
├─ create order
├─ cancel order
├─ policies
├─ DTOs
└─ UI specific to ordersEsto reduce saltos entre carpetas globales components, hooks, services y types donde todo termina mezclado.
// features/orders/index.ts
export { OrdersScreen } from "./ui/orders-screen";
export { listOrdersForCurrentUser } from "./data/list-orders";
export type { OrderListItem } from "./types";Exporta solo lo que otros módulos necesitan. No uses barrels profundos sin control: pueden crear ciclos, empeorar tree shaking y mezclar imports cliente/servidor.
Separación explícita:
orders/data/*.ts → import "server-only"
orders/actions/*.ts → "use server"
orders/ui/*-client.tsx → "use client"Un archivo use client arrastra todos sus imports. No reexportes funciones server-only desde un index consumido por cliente. Puedes mantener entrypoints separados:
features/orders/server
features/orders/clientConsultan y devuelven DTOs autorizados.
Ejecutan transacciones y reglas:
cancelOrder
→ authorize
→ validate current status
→ update transaction
→ audit/outboxAdaptan transporte:
FormData/HTTP
→ schema
→ service
→ UI state/HTTP responseNo hagas HTTP interno desde un Server Component hacia tu propio Route Handler.
shared/ui contiene primitivas sin reglas del dominio:
Un CancelOrderDialog pertenece a orders porque conoce estados y acciones. No conviertas shared en un vertedero.
Una carpeta genérica crece sin límites. Prefiere nombres por responsabilidad:
shared/date
shared/currency
server/auth
server/cacheUna función usada una sola vez puede quedarse junto al consumidor. Reutilización prematura crea APIs pobres.
Coloca juntos:
component.tsx
component.module.css
component.test.tsxTests y schemas cerca reducen navegación. Separa solo artefactos con lifecycle/ownership diferente.
Regla útil:
app → features → shared
features → server infrastructure through narrow APIs
shared ✗ imports featureEvita que una feature importe internals de otra. Usa su public API o extrae una capacidad compartida real.
Los barrels y callbacks entre features pueden crear:
orders → customers → ordersRompe el ciclo mediante:
No introduzcas interfaces para cada clase por reflejo.
Centraliza env validada y clients singleton/gestionados. No leas process.env por toda la app ni crees DB client por request.
import "server-only";
export const db = createDatabaseClient(env.DATABASE_URL);El lifecycle real depende del runtime y ORM.
No confíes en que cada page agregue filtro. Define funciones autorizadas:
listOrdersForOrganization(principal, filters)La arquitectura debe hacer difícil consultar sin tenant. Tests de integración lo verifican.
Señales para dividir:
No dividas solo por número de líneas; una función larga coherente puede ser más clara que cinco capas triviales.
Para muchos productos, una sola app desplegable con módulos claros es suficiente:
one deployment
+ one codebase
+ explicit feature boundariesNo necesitas microservicios para separar responsabilidades. Extrae un servicio cuando runtime, escala, ownership o confiabilidad lo exijan.
Inicio:
app + small feature functionsCrecimiento:
feature folders + DAL + servicesComplejidad real:
separate packages/services where justifiedArquitectura debe poder crecer sin pagar todas las capas desde el día uno.
Difícil reutilizar y probar.
Una feature queda distribuida por todo el repo.
Ciclos y boundaries rotas.
Se convierte en feature oculta.
No expresa políticas.
Añade navegación y contratos sin riesgo real.
Puede arrastrar server-only.
app compone; features contienen capacidad.CancelOrderDialog?Next.js como Backend for Frontend define hasta dónde llegan estas responsabilidades dentro del deployment web.