• Sobre mí
  • Proyectos
  • Contacto
  • Blog

Apasionado por el desarrollo web y el diseño de interfaces intuitivas.

Cada error es un paso. Cada línea, un aprendizaje.

📍   Salou, Tarragona, España ✉️   frankuxui.dev@gmail.com 📞   +34 641932611

Curriculum

Ver mi curriculum completo

Mapa del sitio

  • Inicio
  • Blog
  • Proyectos
  • Sobre mi
  • Mi misión
  • Sitemap

Contácto

  • Página de contácto
  • LinkedIn
  • GitHub

2026 Frankuxui - Todos los derechos reservados

  • Términos y condiciones
  • Política de privacidad
  • Política de cookies
Imagen de Unsplash creada por Milan Malfait
Imagen de Unsplash creada por Milan Malfait
  • Next.js
  • Arquitectura
  • SSR

Articulo publicado el 27 de junio de 2026

Por Frank Esteban Isdray Junco

Arquitectura en Next.js con SSR, Server Components y Client Components

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.

Principio base

La regla practica es simple:

  • Renderiza en el servidor todo lo que no necesite interaccion.
  • Lleva al cliente solo lo que dependa de estado, eventos o APIs del navegador.
  • Mantén los limites entre ambos mundos pequeños y explicitos.

Eso evita dos errores muy comunes:

  1. Convertir toda la pagina en use client por comodidad.
  2. Meter acceso a datos y logica de infraestructura dentro de componentes interactivos.

Estructura recomendada

Una organizacion por dominio suele funcionar mejor que una por tipo de archivo suelto. Por ejemplo:

bash
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.

SSR primero

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.

tsx
// 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.

Separar Server Components y Client Components

Un Server Component es la opcion por defecto. Debe ocuparse de:

  • leer datos
  • componer la pagina
  • pasar props ya resueltas

Un Client Component debe reservarse para:

  • formularios
  • menus desplegables
  • filtros interactivos
  • modales
  • estado local
  • useEffect, useState, window, localStorage

Ejemplo correcto

tsx
// 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>
  );
}
tsx
// 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.

Layouts por contexto

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 global
  • app/(marketing)/layout.tsx para la parte publica
  • app/dashboard/layout.tsx para el area privada
tsx
// 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>
  );
}
tsx
// 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.

Buenas practicas que sí reducen problemas

1. Mantener use client lo mas abajo posible

Si un layout entero entra en cliente, arrastras mas JavaScript del necesario. Lo correcto es aislar solo el componente que lo necesita.

2. No importar codigo sensible en cliente

Si un modulo usa secretos, tokens o accesos internos, debe ser servidor puro.

ts
// 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}`
    }
  });
}

3. Separar fetch y presentacion

No mezcles la obtencion de datos con el render visual si puedes evitarlo.

tsx
// 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();
}
tsx
// 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>
  );
}

4. Usar loading.tsx para rutas pesadas

Si una ruta depende de datos lentos, un estado de carga por segmento mejora mucho la sensacion de velocidad.

tsx
// app/dashboard/loading.tsx
export default function Loading() {
  return (
    <main>
      <p>Cargando dashboard...</p>
    </main>
  );
}

5. Crear limites claros para errores y ausencia de datos

error.tsx y not-found.tsx no son extras decorativos. Son parte de la arquitectura de la ruta.

tsx
// app/dashboard/not-found.tsx
export default function NotFound() {
  return <p>No se encontro el recurso solicitado.</p>;
}

Ejemplo de pagina publica y pagina privada

Una app bien organizada suele tener una superficie publica y otra autenticada.

tsx
// 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>
  );
}
tsx
// 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:

  • la home publica puede ser estatica o parcialmente dinamica
  • el dashboard normalmente necesita SSR y datos por usuario
  • los controles de interaccion se quedan en componentes pequenos

Regla practica final

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.

Cierre

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.

En este artículo

  1. Principio base
  2. Estructura recomendada
  3. SSR primero
  4. Separar Server Components y Client Components
  5. Ejemplo correcto
  6. Layouts por contexto
  7. Buenas practicas que sí reducen problemas
  8. 1. Mantener use client lo mas abajo posible
  9. 2. No importar codigo sensible en cliente
  10. 3. Separar fetch y presentacion
  11. 4. Usar loading.tsx para rutas pesadas
  12. 5. Crear limites claros para errores y ausencia de datos
  13. Ejemplo de pagina publica y pagina privada
  14. Regla practica final
  15. Cierre

Articulos relacionados

Estos articulos pueden interesarte si te gusto este articulo.

Fotografía de Unsplash creada por Michael Busch
Imagen del articulo Configurando Shiki Syntax Highlighter en Next.js con MDX
27 de junio de 2026
Next.js
·React
·Shiki
·MDX
·SSR

Configurando Shiki Syntax Highlighter

Configurando y creando un componente para resaltar la sintaxis de código en un blog utilizando Shiki y Next.js con server-side rendering.

Fotografía de Unsplash creada por Alessio Furlan
Imagen del articulo Configurando Tailwind CSS 4 en Next.js
27 de junio de 2026
Next.js
·Tailwind CSS
·CSS

Tailwind CSS 4 en Next.js: configuración paso a paso

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