Saltar al contenido principal

Cómo Estructuro las Carpetas en un Proyecto Grande de React/Next.js (Vida Real)

La estructura de carpetas que uso en proyectos React y Next.js en producción con +50 componentes. Feature-based, sin over-engineering, con ejemplos reales del árbol completo.

Fran Cobos 8 min de lectura 1459 palabras

Tabla de contenidos

Pregúntale a ChatGPT “cómo estructurar un proyecto React” y te dará esto:

src/
├── components/
├── hooks/
├── utils/
├── services/
├── types/
└── styles/

Esta estructura funciona… para un proyecto con 10 archivos. Cuando llegas a 50+ componentes, 20 hooks y 30 utils, abrir la carpeta components/ es como abrir el cajón de los cubiertos de tu abuela: hay de todo, no encuentras nada, y tienes miedo de tocar algo.

El problema de organizar por tipo de archivo

En uno de mis primeros proyectos grandes (un SaaS de gestión de tareas), tenía esta estructura “por tipo”:

src/
├── components/
│   ├── Button.tsx
│   ├── Modal.tsx
│   ├── TaskCard.tsx
│   ├── TaskForm.tsx
│   ├── TaskList.tsx
│   ├── TaskFilters.tsx
│   ├── ProjectSidebar.tsx
│   ├── ProjectHeader.tsx
│   ├── ProjectSettings.tsx
│   ├── UserAvatar.tsx
│   ├── UserProfile.tsx
│   ├── UserSettings.tsx
│   ├── InvoiceTable.tsx
│   ├── InvoiceForm.tsx
│   ├── ... (47 archivos más)
├── hooks/
│   ├── useTasks.ts
│   ├── useProjects.ts
│   ├── useAuth.ts
│   ├── useInvoices.ts
│   ├── ... (18 archivos más)
├── utils/
│   ├── formatDate.ts
│   ├── formatCurrency.ts
│   ├── taskHelpers.ts
│   ├── projectHelpers.ts
│   ├── ... (12 archivos más)
└── types/
    ├── task.ts
    ├── project.ts
    ├── user.ts
    └── invoice.ts

Para implementar una feature nueva de tareas, tenía que editar archivos en 5 carpetas diferentes: components/, hooks/, utils/, types/ y store/. Cada vez que abría VS Code, la barra lateral parecía una sopa de letras.

La estructura que uso ahora: Feature-Based

src/
├── app/                          # Rutas de Next.js (solo routing)
│   ├── (auth)/
│   │   ├── login/page.tsx
│   │   └── register/page.tsx
│   ├── (dashboard)/
│   │   ├── layout.tsx
│   │   ├── page.tsx              # Dashboard home
│   │   ├── tasks/
│   │   │   ├── page.tsx          # Lista de tareas
│   │   │   └── [id]/page.tsx     # Detalle de tarea
│   │   ├── projects/
│   │   │   ├── page.tsx
│   │   │   └── [id]/page.tsx
│   │   └── settings/
│   │       └── page.tsx
│   ├── api/                      # API routes
│   │   ├── tasks/route.ts
│   │   ├── projects/route.ts
│   │   └── webhooks/stripe/route.ts
│   ├── layout.tsx
│   └── page.tsx                  # Landing page

├── features/                     # 🔑 La clave: lógica agrupada por feature
│   ├── tasks/
│   │   ├── components/
│   │   │   ├── TaskCard.tsx
│   │   │   ├── TaskForm.tsx
│   │   │   ├── TaskList.tsx
│   │   │   └── TaskFilters.tsx
│   │   ├── hooks/
│   │   │   ├── useTasks.ts
│   │   │   └── useTaskMutations.ts
│   │   ├── lib/
│   │   │   ├── task-helpers.ts
│   │   │   └── task-validation.ts
│   │   ├── types.ts
│   │   └── index.ts              # Re-exports públicos
│   │
│   ├── projects/
│   │   ├── components/
│   │   │   ├── ProjectSidebar.tsx
│   │   │   ├── ProjectHeader.tsx
│   │   │   └── ProjectSettings.tsx
│   │   ├── hooks/
│   │   │   └── useProjects.ts
│   │   ├── types.ts
│   │   └── index.ts
│   │
│   ├── auth/
│   │   ├── components/
│   │   │   ├── LoginForm.tsx
│   │   │   └── RegisterForm.tsx
│   │   ├── hooks/
│   │   │   └── useAuth.ts
│   │   ├── lib/
│   │   │   └── auth-config.ts
│   │   └── index.ts
│   │
│   └── billing/
│       ├── components/
│       │   ├── PricingTable.tsx
│       │   ├── InvoiceList.tsx
│       │   └── SubscriptionBadge.tsx
│       ├── hooks/
│       │   └── useSubscription.ts
│       ├── lib/
│       │   └── stripe.ts
│       └── index.ts

├── components/                   # Componentes compartidos (UI primitivos)
│   ├── ui/
│   │   ├── Button.tsx
│   │   ├── Input.tsx
│   │   ├── Modal.tsx
│   │   ├── Skeleton.tsx
│   │   ├── Badge.tsx
│   │   └── Card.tsx
│   └── layout/
│       ├── Navbar.tsx
│       ├── Sidebar.tsx
│       └── Footer.tsx

├── lib/                          # Utilidades globales
│   ├── db.ts                     # Cliente de Prisma
│   ├── api.ts                    # Fetch wrapper
│   ├── utils.ts                  # cn(), formatDate(), etc.
│   └── constants.ts

├── hooks/                        # Hooks globales (no específicos de una feature)
│   ├── useMediaQuery.ts
│   └── useDebounce.ts

└── types/                        # Tipos globales
    └── globals.d.ts

Las reglas que sigo

1. Una feature = una carpeta con todo lo suyo

Cada carpeta en features/ tiene TODO lo que necesita esa funcionalidad: componentes, hooks, utils, tipos. Si mañana quiero eliminar la feature de billing, borro features/billing/ y listo.

features/tasks/
├── components/    # Solo componentes de tareas
├── hooks/         # Solo hooks de tareas
├── lib/           # Solo utils de tareas
├── types.ts       # Solo tipos de tareas
└── index.ts       # Lo que otras features pueden importar

2. El index.ts controla la API pública de la feature

// features/tasks/index.ts
export { TaskList } from './components/TaskList';
export { TaskCard } from './components/TaskCard';
export { useTasks } from './hooks/useTasks';
export type { Task, CreateTaskInput } from './types';

// NO exporto componentes internos como TaskFilters
// — es un detalle de implementación de TaskList

Esto significa que desde fuera, importas así:

// ✅ Correcto — importa desde el barrel
import { TaskList, useTasks } from '@/features/tasks';

// ❌ Evitar — acoplamiento a la estructura interna
import { TaskList } from '@/features/tasks/components/TaskList';

3. components/ global = solo UI primitivos sin lógica de negocio

Si un componente tiene lógica de negocio (fetch datos, validaciones específicas, etc.), va en features/. Si es un componente genérico reutilizable (Button, Modal, Input, Card), va en components/ui/.

// components/ui/Button.tsx — genérico, sin lógica de negocio
export function Button({ children, variant, ...props }) { ... }

// features/tasks/components/TaskCard.tsx — específico de tareas
export function TaskCard({ task }: { task: Task }) {
  // Conoce el tipo Task, formatea fechas específicas, tiene acciones de tareas
}

4. app/ es solo routing — sin lógica

Las páginas en app/ son thin wrappers que importan features:

// app/(dashboard)/tasks/page.tsx
import { TaskList } from '@/features/tasks';
import { getCurrentUser } from '@/features/auth';

export default async function TasksPage() {
  const user = await getCurrentUser();
  return <TaskList userId={user.id} />;
}

3 líneas. Sin lógica. La página solo decide qué componente de feature renderizar y le pasa los datos necesarios.

5. Imports con alias @/ siempre

// tsconfig.json
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
// ✅ Claro, corto, no se rompe al mover archivos
import { Button } from '@/components/ui/Button';
import { TaskList } from '@/features/tasks';

// ❌ Infierno de puntos relativos
import { Button } from '../../../components/ui/Button';
import { TaskList } from '../../features/tasks/components/TaskList';

Cuándo empiezo con esta estructura

No desde el minuto 1. Para un proyecto nuevo o un MVP, empiezo plano:

src/
├── app/
├── components/        # Todo junto al principio
├── lib/
└── types/

Cuando llego a ~20 componentes o cuando noto que toco 4+ carpetas para una feature, es el momento de refactorizar a feature-based. Antes de eso es over-engineering.

La migración es gradual:

  1. Creo features/tasks/
  2. Muevo los archivos relacionados con tareas
  3. Actualizo los imports
  4. Repito para la siguiente feature

VS Code con “Move to file” y el rename de TypeScript actualiza los imports automáticamente.

Errores que he cometido

Error 1: Carpetas dentro de carpetas dentro de carpetas

// ❌ Demasiada anidación
features/tasks/components/list/items/card/TaskCard.tsx

Si necesitas más de 3 niveles de profundidad, algo va mal. La carpeta features/tasks/components/ con archivos planos funciona perfectamente hasta 15-20 componentes. Si necesitas más, probablemente la feature es demasiado grande y habría que dividirla.

Error 2: Features que se importan entre sí de forma circular

// ❌ features/tasks importa de features/projects
// Y features/projects importa de features/tasks
// → Dependencia circular

Si dos features se necesitan mutuamente, hay algo que debería estar en lib/ o en una feature compartida. Los tipos compartidos van a types/, los utils compartidos a lib/.

Error 3: Mover TODO a features/ desde el principio

No todo necesita ser una feature. Si solo tienes un componente de auth (LoginForm) y un hook (useAuth), no necesitas una carpeta features/auth/ con subcarpetas. Ponlo en components/ y cuando crezca, muévelo.

La estructura para un proyecto de portfolio o blog

Para proyectos pequeños (portfolios, blogs, landing pages), esta estructura es overkill. Para esos uso:

src/
├── app/
│   ├── page.tsx
│   ├── blog/
│   │   ├── page.tsx
│   │   └── [slug]/page.tsx
│   └── layout.tsx
├── components/
│   ├── Hero.tsx
│   ├── ProjectCard.tsx
│   ├── BlogPost.tsx
│   └── ContactForm.tsx
├── lib/
│   ├── posts.ts        # Fetch de posts del blog
│   └── utils.ts
└── content/
    └── posts/           # Markdown files

Plano, simple, suficiente. La estructura feature-based solo tiene sentido cuando hay múltiples dominios de negocio (users, tasks, billing, projects…).

Artículos relacionados

Fran Cobos

Fran Cobos

Desarrollador Full Stack especializado en IA aplicada, automatización y desarrollo web. Escribo sobre herramientas, tutoriales y casos reales para programadores.

¿Necesitas desarrollo a medida?

Apps web, IA, módulos ERP — cuéntame tu proyecto.