Como organizo los archivos de un proyecto: el modulo manda, la capa es el detalle
Abri un proyecto de hace ocho meses y tuve los seis archivos que necesitaba en un minuto, sin buscar. No es memoria: es que el nombre del modulo se repite en cada capa, del controlador a la pagina de React.
Abrí un proyecto que no tocaba desde hacía ocho meses porque el cliente quería un cambio en la facturación. Tardé menos de un minuto en tener abiertos los seis archivos que necesitaba, sin buscar nada.
No es memoria. Es que todos mis proyectos se organizan igual, y esa organización tiene una sola regla de fondo.
La regla: el módulo manda, la capa es el detalle
Laravel viene organizado por tipo de archivo. Todos los controladores juntos, todos los modelos juntos, todos los form requests juntos. Funciona perfecto hasta que el proyecto tiene doce módulos y la carpeta de controladores tiene ochenta archivos ordenados alfabéticamente, donde el de facturación está entre el de eventos y el de inventario.
Lo que hago es meter una carpeta más, con el nombre del módulo, dentro de cada capa:
app/
Http/
Controllers/Gestion/
Requests/Gestion/
Resources/Gestion/
Actions/Gestion/
resources/js/pages/gestion/
El mismo nombre, repetido en cada nivel. Cuando busco algo de "gestión", no busco un archivo: sé el prefijo de la ruta y el resto es obvio.
Lo importante no es que sea bonito. Es que borrar un módulo completo son cinco carpetas, y que dos personas trabajando en módulos distintos casi nunca tocan el mismo archivo.
Un módulo por dentro
Así se ve facturación en el POS, de fuera hacia dentro:
app/Http/Controllers/FacturacionController.php recibe y responde
app/Http/Requests/Facturacion/ valida la entrada
app/Actions/Facturacion/
GenerarFacturaDesdeVentaAction.php una accion, un caso de uso
GenerarFacturacionGlobalAction.php
CancelarFacturacionConSwAction.php
app/Services/Facturacion/
ServicioFacturacionSw.php habla con el PAC
FacturaPdfRenderer.php arma el documento
ValidarVentanaFacturacionService.php una regla de negocio
app/Models/ Factura, FacturaConcepto...
config/facturacion.php credenciales y modo
Cada carpeta responde una pregunta distinta:
Controllers — el controlador es delgado a propósito. Recibe, delega, responde. Si un controlador pasa de unas cien líneas, casi siempre es que adentro hay una acción que no he sacado.
Requests — la validación no vive en el controlador. Aparte de limpiar el código, un form request es el único lugar donde busco cuando alguien pregunta "¿qué campos acepta esto?".
Actions — una clase, un caso de uso, un método público. El nombre es una frase completa: GenerarFacturacionGlobalAction. La prueba de que está bien puesta es que el nombre alcanza para entender qué hace sin abrirla.
Services — lo que no es un caso de uso sino una capacidad: hablar con un tercero, renderizar un documento, aplicar una regla. Una acción usa servicios; un servicio no conoce acciones.
En los proyectos más nuevos esta carpeta se llama app/Support/ y va partida por dominio (Support/Alcance/, Support/Panorama/). Es el mismo lugar con otro nombre: "Services" acabó significando cualquier cosa.
Enums, Rules, Resources: las carpetas que valen más de lo que parecen
app/Enums/ es donde vive el vocabulario del proyecto. Estados, roles, niveles, tipos de documento. Antes eran constantes sueltas o, peor, cadenas escritas a mano en veinte archivos.
Un aviso que aprendí a la mala: **si un enum de código representa algo que también existe como catálogo en base de datos, tienes dos fuentes de verdad.**El día que alguien crea una fila que el enum no reconoce, no hay error — hay un null silencioso y un comportamiento raro que nadie asocia con eso.
app/Http/Resources/, también por módulo, es la forma en que los datos salen hacia el frontend. Tenerlo separado del modelo significa que puedo cambiar una columna sin romper la aplicación móvil.
app/Rules/, app/Exports/, app/Imports/, app/Jobs/: cada una existe cuando hace falta y ninguna antes.
config/ no es solo credenciales
Esta es la parte de mi estructura que más me ha servido y la que menos veo en otros proyectos. Cada módulo con decisiones propias tiene su archivo en config/, y ahí adentro escribo por qué:
// config/canal_web.php
/*
| El canal web registra sus pedidos en una sucursal virtual para separarlos
| de la operacion de mostrador. El bot de WhatsApp NO usa este canal: sus
| pedidos caen en la sucursal fisica, asi que esta se mantiene apagada para
| que no ensucie selectores, reportes ni inventarios.
|
| Ponlo en true el dia que se encienda la tienda en linea.
*/
'sucursal_virtual' => [
'activa' => (bool) env('CANAL_WEB_SUCURSAL_VIRTUAL_ACTIVA', false),
],
Ese comentario es más valioso que el código que lo acompaña. Dentro de un año, cuando alguien —yo— se pregunte por qué hay una sucursal apagada, la respuesta está en el mismo archivo donde está el interruptor. No en un ticket, no en un chat, no en la cabeza de nadie.
Mi regla: si una decisión tiene un porqué que no se deduce del código, el porqué va junto a la decisión.
El frontend es un espejo
resources/js/
pages/gestion/ una carpeta por modulo, igual que el backend
components/facturacion/ componentes de un solo modulo
components/ui/ los compartidos, de shadcn
actions/ generado por Wayfinder, no se toca
routes/ generado, tampoco
Que pages/ y Controllers/ usen los mismos nombres no es cosmética: significa que puedo saltar de la vista al controlador sin buscar.
actions/ y routes/ los genera Wayfinder a partir de las rutas de PHP. Son código generado: se leen, no se editan, y no vale la pena revisarlos en un pull request.
Los componentes compartidos son una decisión, no una carpeta
Hay cinco componentes que en todos mis proyectos son siempre los mismos: el diálogo de formulario, los campos de entrada, el botón con estado de carga, el diálogo de confirmación de borrado y el selector de archivos.
Están escritos una vez y se copian tal cual. No porque sean geniales, sino porque cuando el formulario de clientes y el de proveedores se comportan distinto, el usuario lo nota aunque no sepa decir qué le molesta.
docs/ y el archivo de reglas
En la raíz de cada proyecto hay un CLAUDE.md o AGENTS.md. Empezó como instrucciones para asistentes de código y acabó siendo lo más parecido a una guía de estilo que he tenido: dónde va cada cosa, qué componente usar para qué, qué no hacer.
Sirve igual para una persona nueva. De hecho sirve más: nadie lee un wiki, pero todos abren el archivo que está en la raíz del repositorio.
Y docs/ con archivos numerados:
docs/
01-contexto-y-proyectos.md
03-plan-migracion.md
04-decisiones-y-pendientes.md
07-ingesta-bot-whatsapp.md
El número no es orden de importancia, es orden cronológico de cuándo hizo falta pensar en eso. Leídos en orden, cuentan cómo llegó el proyecto a donde está. El 04, el de decisiones y pendientes, es el que más consulto.
Lo que dejo fuera del proyecto
La carpeta docker/ con su Dockerfile y su nginx.conf vive en el servidor, no en el repositorio de la aplicación. Es una decisión discutible —mucha gente prefiere tenerla dentro— y la tomé porque el mismo código se despliega en sitios con necesidades distintas, y no quería que el compose de producción viajara en el repo.
Lo que sí va dentro, cuando existe, es un subproyecto completo con su propio package.json: por ejemplo el gateway de WhatsApp en Node del POS. Es otro lenguaje y otro ciclo de vida, pero es de esa aplicación y de ninguna otra.
Cuándo esta estructura estorba
En un proyecto de tres pantallas, partir por módulos es ceremonia sin beneficio. Ahí uso el Laravel de fábrica y ya.
El punto donde vale la pena es más o menos cuando aparece el tercer módulo, o cuando entra la segunda persona al proyecto. Antes de eso estás pagando por adelantado un orden que todavía no necesitas.
Y una advertencia sobre el "por módulo": funciona mientras los módulos sean del negocio —facturación, inventario, gestión— y no técnicos. Una carpeta llamada Helpers/ o Common/ es donde van a parar las cosas que nadie supo dónde poner, y crece hasta que ya no significa nada.