Articulo publicado el
Guia practica para organizar paginas, layouts y limites de renderizado sin mezclar responsabilidades
Construir una aplicacion con Next.js no consiste solo en hacer que las rutas funcionen. La diferencia entre un proyecto que escala y uno que se vuelve frágil suele estar en la arquitectura: donde colocas la logica, que dejas en el servidor, que mandas al cliente y como organizas paginas y layouts.
En App Router, Next.js favorece una estructura natural para separar responsabilidades. Las paginas y layouts son Server Components por defecto, y eso cambia la forma correcta de pensar el proyecto. Si usas esa base bien, puedes mantener SSR, reducir JavaScript enviado al navegador y conservar una experiencia rapida y predecible.
La regla practica es simple:
Eso evita dos errores muy comunes:
use client por comodidad.Una organizacion por dominio suele funcionar mejor que una por tipo de archivo suelto. Por ejemplo:
app/
layout.tsx
page.tsx
(marketing)/
page.tsx
layout.tsx
dashboard/
layout.tsx
page.tsx
loading.tsx
not-found.tsx
src/
features/
dashboard/
components/
services/
sections/
home/
blog/
components/
ui/
theme-provider.tsx
infrastructure/
strapi/
lib/
auth/
posts/
Esta estructura separa bien tres capas:
app/ define rutas, layouts y limites de renderizado.src/features/ contiene la logica de cada dominio de negocio.src/components/ deja UI compartida, pero no logica de negocio.Si una pagina necesita datos para ser util desde el primer paint, debe renderizarse en servidor. Eso te permite usar credenciales privadas, consultar APIs internas y devolver HTML ya listo.
// app/dashboard/page.tsx
import { getDashboardSummary } from "@/src/features/dashboard/services/get-dashboard-summary";
export default async function DashboardPage() {
const summary = await getDashboardSummary();
return (
<main>
<h1>Dashboard</h1>
<p>Pedidos: {summary.orders}</p>
<p>Ingresos: {summary.revenue}</p>
</main>
);
}
La idea no es "usar SSR porque si". La idea es usarlo cuando el contenido depende del servidor y el navegador no aporta nada para construir la primera vista.
Un Server Component es la opcion por defecto. Debe ocuparse de:
Un Client Component debe reservarse para:
useEffect, useState, window, localStorage// app/dashboard/page.tsx
import Filters from "./filters";
import { getOrders } from "@/src/features/dashboard/services/get-orders";
export default async function DashboardPage() {
const orders = await getOrders();
return (
<main>
<h1>Pedidos</h1>
<Filters />
<ul>
{orders.map((order) => (
<li key={order.id}>
{order.number} - {order.status}
</li>
))}
</ul>
</main>
);
}
// app/dashboard/filters.tsx
"use client";
import { useState } from "react";
export default function Filters() {
const [status, setStatus] = useState("all");
return (
<label>
Estado
<select value={status} onChange={(event) => setStatus(event.target.value)}>
<option value="all">Todos</option>
<option value="pending">Pendientes</option>
<option value="paid">Pagados</option>
</select>
</label>
);
}
El servidor carga los datos y el cliente solo maneja la interaccion necesaria.
Los layouts no deberian usarse como una carpeta de relleno. Su funcion es mantener estructura comun entre rutas relacionadas.
Por ejemplo:
app/layout.tsx para el shell globalapp/(marketing)/layout.tsx para la parte publicaapp/dashboard/layout.tsx para el area privada// app/layout.tsx
import type { ReactNode } from "react";
import ThemeProvider from "@/src/components/theme-provider";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="es">
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}
// app/dashboard/layout.tsx
import type { ReactNode } from "react";
import DashboardSidebar from "@/src/features/dashboard/components/dashboard-sidebar";
export default function DashboardLayout({ children }: { children: ReactNode }) {
return (
<div className="dashboard-shell">
<DashboardSidebar />
<section>{children}</section>
</div>
);
}
La ventaja de este enfoque es que el layout comparte navegación, contexto visual y elementos persistentes sin obligarte a repetirlos en cada pagina.
use client lo mas abajo posibleSi un layout entero entra en cliente, arrastras mas JavaScript del necesario. Lo correcto es aislar solo el componente que lo necesita.
Si un modulo usa secretos, tokens o accesos internos, debe ser servidor puro.
// src/infrastructure/strapi/client.ts
import "server-only";
export async function getPrivateContent() {
return fetch(`${process.env.STRAPI_URL}/api/posts`, {
headers: {
Authorization: `Bearer ${process.env.STRAPI_READ_ONLY_ACCESS_TOKEN}`
}
});
}
No mezcles la obtencion de datos con el render visual si puedes evitarlo.
// src/features/blog/services/get-posts.ts
export async function getPosts() {
const response = await fetch("https://example.com/api/posts", {
cache: "no-store"
});
return response.json();
}
// src/features/blog/sections/blog-list.tsx
import { getPosts } from "../services/get-posts";
export default async function BlogList() {
const posts = await getPosts();
return (
<div>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
);
}
loading.tsx para rutas pesadasSi una ruta depende de datos lentos, un estado de carga por segmento mejora mucho la sensacion de velocidad.
// app/dashboard/loading.tsx
export default function Loading() {
return (
<main>
<p>Cargando dashboard...</p>
</main>
);
}
error.tsx y not-found.tsx no son extras decorativos. Son parte de la arquitectura de la ruta.
// app/dashboard/not-found.tsx
export default function NotFound() {
return <p>No se encontro el recurso solicitado.</p>;
}
Una app bien organizada suele tener una superficie publica y otra autenticada.
// app/(marketing)/page.tsx
export default function MarketingHome() {
return (
<main>
<h1>Producto</h1>
<p>Landing publica con contenido estatico y SSR si hace falta.</p>
</main>
);
}
// app/dashboard/page.tsx
import { getSession } from "@/src/features/auth/services/get-session";
export default async function DashboardHome() {
const session = await getSession();
return (
<main>
<h1>Hola, {session.user.name}</h1>
<p>Area privada con datos personalizados.</p>
</main>
);
}
Ese contraste ayuda a decidir donde vive cada cosa:
Si una pieza de UI puede funcionar sin navegador, dejala en servidor. Si necesita interaccion real, aislala en cliente. Si comparte estructura entre varias rutas, conviertela en layout. Si solo sirve a un dominio concreto, guardala dentro de su feature.
Ese criterio simple suele dar una base mucho mas limpia que intentar "componentizar" todo por igual.
Next.js funciona especialmente bien cuando respetas la naturaleza de cada capa. SSR te da la primera respuesta, Server Components te ayudan a contener la logica y Client Components solo aparecen donde hacen falta. A partir de ahi, layouts y rutas anidadas te permiten crecer sin perder orden.
Si diseñas el proyecto con estos limites desde el inicio, el resultado es mas facil de mantener, mas rapido de cargar y mas sencillo de evolucionar cuando el producto crece.
Estos articulos pueden interesarte si te gusto este articulo.
Configurando y creando un componente para resaltar la sintaxis de código en un blog utilizando Shiki y Next.js con server-side rendering.
Guía completa para integrar Tailwind CSS 4 en un proyecto de Next.js utilizando la configuración oficial recomendada, optimizada para rendimiento y escalabilidad