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.
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:
- Creo
features/tasks/ - Muevo los archivos relacionados con tareas
- Actualizo los imports
- 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
- Por qué dejé de usar Redux (y qué uso ahora) — cómo organizo el estado sin Redux
- Caso real: SaaS con NestJS y React — esta estructura en un proyecto real
- Auth.js (NextAuth) sin volverte loco — cómo estructuro la feature de auth