El proyecto será un restaurante
Un restaurante no funciona porque todos hagan de todo. Funciona porque cada uno tiene un puesto. El proyecto es igual. Todo lo que viene ahora es solamente decir qué puesto le toca a cada archivo.
| En el restaurante | En el proyecto | Su regla |
|---|---|---|
| La carta | routes/ | Dice qué se puede pedir. No prepara nada. |
| El anfitrión | middlewares/ | Dice «pasa» o «no pasa». Nunca cocina. |
| El mesero | controllers/ | Lleva y trae. No decide la receta. |
| La cocina | services/ | Tiene las recetas. Nunca atiende mesas. |
| La bodega | prisma/ | Guarda y entrega. No decide nada. |
| El comedor | client/ | Todo lo que el cliente ve y toca. |
| Los salones | pages/ | Cada uno es una pantalla con su dirección. |
| La loza | components/ | Las piezas que se repiten en todas las mesas. |
| El mesón | services/ (front) | El único lugar por donde salen los pedidos. |
1.1El viaje de un pedido
Alguien aprieta «Reservar». Esto es lo que pasa adentro, puesto por puesto. Avanza con el botón o toca cualquier parada.
1.2La prueba de fuego
Hay una sola pregunta que decide si una regla está en el lugar correcto:
¿Podrías llamar a la regla «una mesa no se reserva dos veces en el mismo bloque» desde un script que corre a medianoche, sin navegador, sin nadie conectado y sin ninguna petición HTTP?
- Si es sí — la regla vive en
services/como una función que recibe datos y devuelve datos. Está donde corresponde. - Si necesita
reqyres— quedó atrapada dentro del controlador. Yreqyressolo existen mientras hay una petición HTTP en vuelo, o sea solo cuando alguien apretó un botón.
Las reglas del negocio no dependen de que la petición venga por HTTP. Podrían venir de un script, de una tarea programada, de una prueba automatizada o de otro endpoint. Si la regla solo corre cuando llega un req, la amarraste a un detalle técnico que no tiene nada que ver con el negocio.
«No se puede reservar con fecha pasada», «el total incluye IVA», «un usuario no puede aprobar su propia solicitud». Todas esas son de la cocina.
1.3Las cuatro ideas, con los nombres que corresponden
Separación de responsabilidades
routes → middlewares → controllers → services → ORM. La lógica de negocio no conoce req ni res, y por eso se puede llamar desde cualquier parte.
El frontend no trae estructura
El criterio lo definen ustedes y queda escrito en el README. Toda llamada HTTP pasa por la capa de servicios, y la URL del backend vive en una variable de entorno.
Props abajo, eventos arriba
El contrato de props es la interfaz pública de un componente. Y no lo diseñen antes de la tercera repetición: abstraer mal acopla más que duplicar.
Una familia, una librería, un wrapper
Elijan de qué familia la quieren, anótenla en el README con su versión, y que components/ui/ sea el único punto de acoplamiento.
¿Dónde va cada archivo, y por qué?
Toca una carpeta para ver su regla, o un archivo para ver qué hace, por qué vive ahí y, cuando corresponde, su código. El proyecto de ejemplo es el sistema de reservas de mesas del repositorio que les pasamos.
Qué es una prop
Ya sabes lo que es una prop. Lo aprendiste en la Ayudantía 1 y llevas años escribiéndolas en HTML. Lo único que falta es el nombre.
Un componente es una función que devuelve interfaz. Las props son sus parámetros.
3.1Puente uno: es un parámetro de función
Esto lo escribiste en la primera ayudantía:
function saludar(nombre) { return 'Hola ' + nombre } saludar('Vicente') saludar('Nelson')
Nadie escribiría una función saludarAVicente() y otra saludarANelson(). El nombre entra como parámetro, y una sola función sirve para los dos.
Un componente funciona exactamente igual:
function Boton({ texto }) { return <button>{texto}</button> } <Boton texto="Guardar" /> <Boton texto="Cancelar" />
- 1
textoes el parámetro. Llega desde afuera y el componente no lo elige. - 2Adentro se usa igual que cualquier variable. El componente no sabe ni le importa qué dice.
- 3Y acá se le pasa el valor. Mismo componente, dos botones distintos.
3.2Puente dos: es un atributo de HTML
Llevas años escribiendo esto sin pensarlo:
<img src="foto.png" alt="Una foto">
src y alt son datos que le pasas a la etiqueta desde afuera. Las props son el mismo gesto, con una diferencia: el nombre lo inventas tú. texto, variante, mesa, lo que el componente necesite.
Si la explicación termina en «son como los atributos de HTML», queda la idea de que una prop solo lleva texto. Una prop puede ser un número, un arreglo, un objeto, una función — onClick es una prop — y hasta otro componente completo.
3.3Pruébalo
Mueve las props de la izquierda y mira el botón y su JSX. Es el mismo componente en los tres casos.
3.4La parte que de verdad cuesta
Las props son de solo lectura. Entran, el componente las usa, y no las puede modificar. Si necesita que algo cambie, no lo cambia él: avisa hacia arriba con un evento y decide quien lo usó.
// el botón solo avisa que lo apretaron function Boton({ children, onClick }) { return <button onClick={onClick}>{children}</button> } // la pantalla decide qué hacer con ese aviso <Boton onClick={guardarReserva}>Guardar</Boton>
De ahí sale la frase props abajo, eventos arriba. El que llama manda; el llamado obedece. Igual que con los argumentos de una función.
3.5El criterio que te tienes que llevar
Todo lo que cambia entre un uso y otro entra por props. Lo que no cambia se queda adentro.
Con esa frase resuelves casi todas las dudas de diseño de un componente. El texto cambia entre un botón y otro, así que es prop. El padding y el border-radius son iguales siempre, así que se quedan adentro.
Poner todo como prop por si acaso. Terminas con un botón de doce props y nadie, ni tú, se acuerda de para qué servía la séptima. Agrega una prop cuando un caso real la pida, no antes. Es la misma regla de las tres veces de la sección 4.
3.6Chequeo rápido
Si puedes responder esto sin mirar, lo tienes.
- Tengo un botón que dice «Guardar» y necesito uno que diga «Cancelar». ¿Qué tiene que entrar por props?El texto.
- ¿Y si además uno es rojo y el otro gris?El color también. Dos props: el texto y la variante.
- ¿Y el
border-radius, que es igual en los dos?Se queda adentro del componente. No cambia entre un uso y otro, así que no es prop.
Los conceptos, ahora con código
En la sala vimos la idea. Acá está escrita. Léelo con calma y con el repositorio de ejemplo abierto al lado.
4.0Los tres tipos de componente
Es la duda que más se repite. Para probarlo, pregúntate:
¿Podrías copiar este archivo a otro proyecto, sin cambiar una línea, y serviría igual?
Si es sí, es un primitivo y va en components/ui/. Si el archivo menciona mesas, reservas, clientes, roles o el logo de ustedes, conoce el dominio y va en components/ directo.
| Nivel | Qué es | Ejemplos | Cómo lo reconoces |
|---|---|---|---|
components/ui/ |
Primitivos. No saben nada de la aplicación. | Boton Input Card Modal Badge |
Reciben props genéricas y dibujan. No importan nada de services/ ni de context/. |
components/ |
Compuestos de dominio. Saben algo de la aplicación, pero no son una pantalla. | DataTable Layout Sidebar FormularioReserva |
Mencionan entidades del dominio, rutas propias, la sesión o la identidad visual del proyecto. Se arman combinando primitivos. |
pages/ |
Contenedores. Tienen ruta propia, piden datos y manejan estado. | MesasPage ReservasPage LoginPage |
Son los únicos que llaman a services/. Lo que consiguen lo pasan hacia abajo por props. |
Los casos que confunden
Boton→ui/. Recibe texto, variante y unonClick. No sabe si guarda una reserva o borra un usuario.Sidebar→components/. Tiene los módulos de este sistema y sabe qué opciones esconder según el rol.DataTable→components/. Aunque es genérica, está pensada para las tablas de este proyecto y usa su estilo. Si la hicieran del todo agnóstica, podría subir aui/.EstadoReserva, la insignia que pinta «confirmada» en verde →components/, porque conoce los estados del negocio. La insignia genérica (Badge, que solo recibe color y texto) sí va enui/.Modal→ui/.ModalConfirmarReserva→components/, porque sabe qué pregunta hacer.RutaProtegida→components/. Lee el contexto de sesión de esta aplicación.
Si un archivo de components/ui/ necesita importar algo de services/, de context/ o de utils/ con reglas de negocio, no era un primitivo. Bájalo a components/.
La separación sirve para algo concreto: components/ui/ es lo que se pueden llevar completo al próximo proyecto, y es donde va a vivir el envoltorio de MUI o ShadCN. Tenerlo aparte deja claro qué es reutilizable y qué está amarrado a este sistema.
4.1El mesero que se metió a la cocina
El caso completo: crear una reserva validando que la mesa no esté ocupada.
export const crear = async (req, res) => { const { mesaId, inicio, fin } = req.body if (!mesaId || !inicio || !fin) return res.status(400).json({ error: 'Faltan campos' }) if (new Date(inicio) >= new Date(fin)) return res.status(400).json({ error: 'Rango invalido' }) const choque = await prisma.reserva.findFirst({ where: { mesaId, inicio: { lt: fin }, fin: { gt: inicio } } }) if (choque) return res.status(409).json({ error: 'Mesa ocupada' }) const r = await prisma.reserva.create({ data: { mesaId, inicio, fin } }) res.status(201).json(r) }
- 1Revisa la forma del formulario. Eso es trabajo del anfitrión de la puerta.
- 2Decide una regla del negocio. Esto es lo grave: la regla es tuya, no de HTTP, y encerrada acá no se puede llamar desde ningún otro lado.
- 3Habla directo con la bodega. Un controlador no debería tener un
prisma.adentro. - 4Y recién acá hace lo suyo: armar la respuesta HTTP.
El peor es el segundo. Toca las anotaciones de arriba para ver dónde está cada uno.
export const crear = async (req, res, next) => { try { const r = await reservaService.crear(req.body) res.status(201).json(r) } catch (e) { next(e) } }
export const crear = async ({ mesaId, inicio, fin }) => { if (new Date(inicio) >= new Date(fin)) throw Object.assign(new Error('Rango invalido'), { status: 400 }) const choque = await prisma.reserva.findFirst({ where: { mesaId, inicio: { lt: fin }, fin: { gt: inicio } } }) if (choque) throw Object.assign(new Error('Mesa ocupada'), { status: 409 }) return prisma.reserva.create({ data: { mesaId, inicio, fin } }) }
// validarReserva.js — revisa la FORMA del pedido export default function validarReserva(req, res, next) { const { mesaId, responsable, inicio, fin } = req.body if (!mesaId || !responsable || !inicio || !fin) return res.status(400).json({ error: 'Faltan campos obligatorios' }) next() } // manejarErrores.js — el único que arma respuestas de error export default function manejarErrores(err, req, res, next) { res.status(err.status || 500).json({ error: err.message }) }
El controlador quedó en cinco líneas. La regla se puede llamar desde otro endpoint o desde un test. El try/catch dejó de repetirse en cada función. Y el formulario se revisa antes de molestar a la cocina.
4.2Cómo se encadena todo en las rutas
import { Router } from 'express' import * as reservaController from '../controllers/reserva.controller.js' import validarReserva from '../middlewares/validarReserva.js' const router = Router() router.get('/', reservaController.listar) router.post('/', validarReserva, reservaController.crear) // ↑ primero el anfitrión, después el mesero export default router
// app.js — arma el restaurante, pero no abre la puerta app.use(express.json()) app.use('/api', rutas) app.use(manejarErrores) // va al final, SIEMPRE export default app // index.js — abre la puerta, y eso es todo app.listen(3000)
Están separados para que puedas importar la app en un test sin ocupar el puerto 3000 de la máquina. Es el mismo motivo por el que un restaurante se puede montar antes de abrir.
4.3Una sola conexión a la bodega
Este es el error más común del semestre. Prisma abre un grupo de conexiones por cada cliente que creas.
const prisma = new PrismaClient() // ← y otro, y otro, y otro…
import { PrismaClient } from '@prisma/client' export const prisma = new PrismaClient() // y en todos los demás archivos: import { prisma } from '../config/prisma.js'
PostgreSQL tiene un límite de conexiones. Con una instancia por archivo, el servidor empieza a rechazar peticiones, normalmente cuando más gente está usando el sistema.
4.4La puerta única al backend
useEffect(() => {
fetch('http://localhost:3000/api/reservas', {
headers: { Authorization: 'Bearer ' + token }
}).then(r => r.json()).then(setReservas)
}, [])
import axios from 'axios' export const api = axios.create({ baseURL: import.meta.env.VITE_API_URL }) // le pega el token a TODAS las peticiones, una sola vez api.interceptors.request.use(cfg => { const token = localStorage.getItem('token') if (token) cfg.headers.Authorization = `Bearer ${token}` return cfg }) // y si el backend dice 401, cierra sesión en un solo lugar api.interceptors.response.use(r => r, err => { if (err.response?.status === 401) { localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(err) })
import { api } from './api.js' export const listar = () => api.get('/reservas').then(r => r.data) export const crear = datos => api.post('/reservas', datos).then(r => r.data)
VITE_API_URL=http://localhost:3000/api en desarrollo, y la dirección de la FACE en producción. Cambiar de servidor es cambiar una línea.
Todo lo que empieza con VITE_ queda visible en el navegador. Nunca pongas una clave secreta en el .env del frontend: no es un secreto, es un cartel.
4.5Un componente reutilizable, completo
function BotonGuardarReserva() { return <button className="btn-azul" onClick={() => guardarReserva()}> Guardar reserva </button> } // sabe qué dice, de qué color es y qué hace. Sirve en un solo lugar.
const estilos = { primario: 'bg-blue-600 text-white', secundario: 'bg-gray-200 text-gray-900', peligro: 'bg-red-600 text-white' } export default function Boton({ children, variante = 'primario', cargando = false, ...props }) { return ( <button className={`px-4 py-2 rounded font-medium ${estilos[variante]}`} disabled={cargando || props.disabled} {...props} > {cargando ? 'Cargando…' : children} </button> ) }
- 1
childrenes qué dice el botón. Entra desde afuera. - 2
variantees de qué color, con un valor por defecto para no repetirlo en cada uso. - 3
cargandolo deshabilita y le cambia el texto mientras espera. - 4
...propsdeja pasar todo lo demás:onClick,type,aria-label. Esto es lo que más se olvida, y sin ello hay que volver a editar el componente el día que necesites untype="submit".
<Boton onClick={guardar} cargando={guardando}>Guardar</Boton>
<Boton variante="peligro" onClick={borrar}>Eliminar</Boton>
<Boton variante="secundario" type="button" onClick={cerrar}>Cancelar</Boton>
4.6children: el hueco que deja el componente
export default function Card({ titulo, acciones, children }) { return ( <section className="card"> <header> <h3>{titulo}</h3> {acciones} // ← otro hueco, pero arriba </header> {children} // ← el hueco principal </section> ) }
<Card titulo="Reservas de hoy" acciones={<Boton>Nueva</Boton>}> <DataTable columnas={cols} datos={reservas} vacio="Sin reservas" /> </Card> <Card titulo="Nueva reserva"> <FormularioReserva onGuardar={crear} /> </Card>
children no tiene que ir al final. Un Layout con menú arriba y contenido abajo pone el hueco entre medio. Y puedes tener varios huecos, como el acciones del ejemplo.
4.7La tabla genérica
export default function DataTable({ columnas, datos, vacio }) { if (!datos.length) return <p className="vacio">{vacio}</p> return ( <table> <thead> <tr>{columnas.map(c => <th key={c.campo}>{c.titulo}</th>)}</tr> </thead> <tbody> {datos.map(fila => ( <tr key={fila.id}> {columnas.map(c => ( <td key={c.campo}>{c.render ? c.render(fila) : fila[c.campo]}</td> ))} </tr> ))} </tbody> </table> ) }
const columnas = [ { campo: 'mesa', titulo: 'Mesa', render: r => r.mesa.numero }, { campo: 'responsable', titulo: 'Responsable' }, { campo: 'inicio', titulo: 'Desde', render: r => formatearHora(r.inicio) } ] <DataTable columnas={columnas} datos={reservas} vacio="Sin reservas para hoy" />
Deja que cada pantalla decida cómo se dibuja una celda, sin que la tabla tenga que saber qué es una mesa.
4.8El hook que evita copiar el mismo useEffect
import { useEffect, useState, useCallback } from 'react' export function useFetch(fn) { const [datos, setDatos] = useState([]) const [cargando, setCargando] = useState(true) const [error, setError] = useState('') const recargar = useCallback(() => { setCargando(true) fn().then(setDatos) .catch(e => setError(e.message)) .finally(() => setCargando(false)) }, [fn]) useEffect(() => { recargar() }, [recargar]) return { datos, cargando, error, recargar } }
Y en la pantalla queda una línea: const { datos: reservas, cargando, recargar } = useFetch(useCallback(reservaService.listar, []))
Cuando el mismo useState + useEffect aparece en tres pantallas. Antes de eso, no. Es la misma regla de las tres veces.
4.9Envolver la librería UI
import { Button } from '@mui/material' export default function Boton({ children, ...props }) { return ( <Button variant="contained" disableElevation {...props}> {children} </Button> ) }
import { Button } from '@/components/ui/button' export default function Boton({ children, ...props }) { return <Button {...props}>{children}</Button> } // las treinta pantallas no se enteran de nada
Migrar de librería pasa a ser reescribir un archivo en vez de ochenta. Y de paso, todos los botones del sistema quedan consistentes sin que nadie tenga que acordarse.
Librerías UI: cuál elegir y por qué
MUI y ShadCN son los ejemplos que usamos en la ayudantía, pero no son dos productos que compitan: son dos maneras distintas de recibir el código. El criterio que sigue sirve para cualquiera de las dos familias.
5.1Qué te da de verdad una librería UI
No es que se vea bonito. Es todo lo que un componente hecho a mano no tiene y nadie se acuerda de programar. El ejemplo más claro es un modal, que parece un div encima de otro.
Lo que le falta
- Con Tab te vas navegando a la página de atrás, que sigue ahí debajo.
- No cierra con Escape, porque nadie se acordó de escuchar esa tecla.
- Al cerrarlo, el foco queda en ninguna parte en vez de volver al botón que lo abrió.
- Un lector de pantalla no anuncia que se abrió una ventana: sigue leyendo la página de atrás.
- El clic en el fondo lo cierra, y el clic adentro también, porque el evento sube.
- La página de atrás sigue haciendo scroll mientras el modal está abierto.
Lo que sí trae
- Focus trap y restauración del foco. El Tab da vueltas dentro del modal, y al cerrarlo el foco vuelve solo al botón que lo abrió.
- Semántica para lectores de pantalla.
role="dialog"yaria-modal, para que quien no ve la pantalla sepa que se abrió una ventana. - Teclado y clics resueltos. Escape cierra, el clic en el overlay cierra, el clic adentro no. Y el scroll de atrás queda bloqueado.
- Probado donde ustedes no van a probar. En Safari, en Android, con teclado y con lector de pantalla.
Lo mismo vale para Select, Dropdown, Tooltip, Tabs y Combobox: parecen simples y tienen semanas de detalles adentro. Eso es lo que están comprando, y es la razón real para usar una librería. El ahorro de tiempo es un efecto secundario.
5.2Dos familias, no dos productos
La librería es una dependencia
MUI Ant Design Chakra UI Mantine
- Instalan un paquete y el código de los componentes vive en
node_modules. - Llegan estilados y con un sistema de diseño ya decidido por la librería.
- Personalizan por donde la librería los deja: un tema central y props de estilo.
- Actualizan con npm y reciben los arreglos que hicieron los demás.
El código termina en su repositorio
ShadCN/ui Radix UI Headless UI
- Un comando copia el archivo del componente dentro de su propio
src/. - Traen el comportamiento y la accesibilidad resueltos, pero los estilos los ponen ustedes.
- Personalizan editando el archivo, porque a partir de ese momento es suyo.
- No hay actualización automática: lo que copiaron es lo que tienen.
Tailwind no es una librería de componentes: es un sistema de clases CSS. La segunda familia lo da por instalado, así que si su grupo no usa Tailwind, esa familia entera se les cae de la lista antes de empezar a comparar.
5.3Dónde queda el código de cada familia
La diferencia práctica no es cómo se ven. Es dónde vive el archivo y quién lo puede editar.
npm install @mui/material
El componente queda en node_modules/, que no entra a Git y que ustedes no editan. A su repositorio entra una línea del package.json.
npx shadcn@latest add button
El componente aparece en src/components/ui/button.jsx y es suyo: entra a Git, sale en sus commits y pasa por su code review como cualquier archivo que hayan escrito.
Con la primera, actualizar es un comando y personalizar tiene un techo. Con la segunda, personalizar no tiene techo y actualizar es volver a copiar el archivo a mano, revisando qué cambió. Ninguna de las dos es mejor: son dos tratos distintos, y conviene saber cuál están firmando.
5.4Criterios para elegir
Seis preguntas. Respóndanlas en grupo, no cada uno por su cuenta.
| La pregunta | Familia 1 · dependencia | Familia 2 · código copiado |
|---|---|---|
| ¿Qué tan rápido necesitan partir? | Muy rápido: llega estilado | Más lento: hay que estilar todo |
| ¿Cuánto van a personalizar? | Hasta donde el tema los deje | Sin techo: el archivo es suyo |
| ¿El grupo usa Tailwind? | No hace falta | Obligatorio, no es opcional |
| ¿Quién mantiene el componente? | La librería, y actualizan con npm | Ustedes, de acá hasta diciembre |
| ¿Cuánto pesa en el navegador? | Suma la librería completa | Solo lo que copiaron |
| ¿Qué pasa al actualizar? | npm update y revisar que no rompa | Copiar de nuevo y comparar a mano |
En el README, con su versión. Y no mezclen: dos librerías de componentes en el mismo proyecto son el doble de peso en el navegador y dos estilos que nunca van a calzar. Es de las pocas decisiones de hoy que cuesta caro deshacer en la semana catorce.
5.5Sea cual sea, va detrás de un wrapper
La librería no debería aparecer importada fuera de components/ui/. Sin wrapper queda importada en treinta archivos, y cambiarla es abrir los treinta. Con wrapper, un solo archivo del proyecto la conoce.
Migrar de librería pasa a ser reescribir un archivo en vez de ochenta. Y aunque nunca cambien de librería, igual ganan: todos los botones del sistema quedan consistentes sin que nadie tenga que acordarse de cuál era la variante correcta.
El código del envoltorio está en la sección anterior, en 4.9.
Organizar por módulo
La otra forma de ordenar el frontend. Conviene conocerla aunque no la usen, porque se la van a topar en cualquier repositorio grande.
Por tipo
Todo lo del mismo tipo junto. Es lo que vimos en la ayudantía.
src/ ├── components/ ├── pages/ ├── hooks/ ├── services/ └── utils/
Por módulo (feature)
Todo lo de un módulo junto, aunque sean tipos distintos.
src/
├── features/
│ ├── reservas/
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── reservaService.js
│ │ └── ReservasPage.jsx
│ └── mesas/
│ ├── components/
│ ├── mesaService.js
│ └── MesasPage.jsx
└── shared/
├── components/ui/
├── services/api.js
└── utils/6.1En el restaurante
Por tipo es ordenar la cocina por tipo de utensilio: todos los cuchillos en un cajón, todas las ollas en otro.
Por módulo es ordenarla por estación: la estación de postres tiene sus propios cuchillos, sus propias ollas y su propio horno.
Las dos formas son válidas. Una cocina chica funciona mejor por tipo de utensilio. Una cocina grande, con un jefe por estación, funciona mejor por estación.
6.2Cuándo conviene cada una
| Por tipo | Por módulo | |
|---|---|---|
| Tamaño del proyecto | Hasta unas 15 o 20 pantallas | De ahí en adelante |
| Cómo se reparte el trabajo | Todos tocan todo | Cada integrante es dueño de un módulo |
| Buscar un archivo | Sabes el tipo, vas a su carpeta | Sabes el módulo, vas a su carpeta |
| Conflictos en Git | Más probables: todos editan components/ | Menos probables: cada uno en lo suyo |
| Lo difícil | components/ se llena y hay que reordenar | Decidir qué es compartido y qué no |
| Curva de entrada | Se entiende sola | Hay que explicarla |
Por tipo alcanza y sobra. Lo importante no es cuál eligen, sino que los cuatro elijan la misma, la anoten en el README y no la cambien a mitad de semestre. Media estructura de cada tipo es peor que cualquiera de las dos completa.
6.3El error típico de «por módulo»
Poner todo dentro de los módulos y quedarse sin lugar para lo compartido.
El botón está copiado en features/reservas/components/Boton.jsx y en features/mesas/components/Boton.jsx. Ahí falta una carpeta shared/ con lo que de verdad usan todos.
Algo se mueve a shared/ cuando lo necesita el segundo módulo, no antes. Otra vez la misma idea: no abstraigas antes de tiempo.
Reordenar lo que ya está andando
Esta sección sí habla del proyecto de ustedes, que ya corre en el servidor de la FACE. Es cómo moverlo de lugar sin romperlo.
No intenten reorganizarlo todo en una tarde, y no dejen de programar una semana entera por esto. Un módulo por sesión, con la aplicación funcionando al final de cada una.
7.1Los cinco pasos
-
Rama aparte, siempre
Nunca directo a
devy muchísimo menos amain. Si algo sale mal, se borra la rama y no pasó nada.git switch -c refactor/estructura
-
Usa git mv, no arrastres carpetas
Si arrastras en el explorador, Git lo ve como un archivo borrado y otro creado: pierdes el historial y la autoría.
git mv src/todo.js src/services/mesaService.js
-
Un movimiento, un commit
Es el mismo criterio de la Ayudantía 1. Si el commit mueve ocho cosas, no puedes revertir solo la que falló.
git commit -m "chore(struct): mueve la logica de reservas a services/"
- Mover no es cambiar Si aprovechas de corregir un bug en el mismo commit y algo se rompe, no vas a saber si fue el movimiento o el arreglo. Primero mueves; en otro commit, arreglas.
-
Levanta el proyecto en cada paso
Un import roto no siempre avisa al compilar. Avisa en el navegador, cuando entras a la pantalla.
npm run dev # en los dos lados, y haz clic de verdad
7.2En qué orden mover las cosas
De abajo hacia arriba, para que cada paso deje el proyecto funcionando:
- Primero el backend, que es más fácilSaca las consultas del controlador a un
service. Un recurso a la vez. - Después services/api.js en el frontendCrea el archivo, y ve cambiando pantalla por pantalla para que use el servicio en vez de llamar directo.
- Después mueve las pantallas a pages/Es solo mover archivos y arreglar imports.
- Al final, extrae los componentes repetidosEsto es lo único que requiere pensar, y por eso va último, cuando el resto ya está ordenado.
Cada paso es reversible por su cuenta y ninguno depende del siguiente. Si se les acaba el tiempo en el paso dos, el proyecto igual quedó mejor que antes.
7.3Errores que van a cometer
- Mover un archivo y olvidar un import. Vite avisa en el navegador, no en la terminal. Hagan clic en todas las pantallas después de cada paso.
- Renombrar de
tabla.jsxaDataTable.jsxen Windows. Git a veces no detecta el cambio de mayúsculas. Si pasa, usengit mv tabla.jsx temporal.jsxy después aDataTable.jsx. - Mover el
.envsin actualizar el.env.example. Al compañero que clone le va a faltar una variable y no va a saber cuál. - Hacer el refactor en la misma rama donde alguien está trabajando. El merge va a ser una pesadilla. Avisen en el grupo antes de empezar.
El repositorio de ejemplo
Un mini sistema de reservas de mesas, hecho dos veces: desordenado y ordenado. Las dos versiones funcionan exactamente igual.
8.1Cómo levantarlo
Necesitas Node 18 o superior. Nada más: ni base de datos ni configuración.
# terminal 1 — el backend cd server npm install npm run dev # queda en http://localhost:3000 # terminal 2 — el frontend cd client npm install npm run dev # queda en http://localhost:5173
Acá la «base de datos» es un arreglo en memoria, para que esto corra sin que instales PostgreSQL. En el proyecto del ramo eso es Prisma, y el único archivo que cambia es config/db.js. Todo lo demás se organiza exactamente igual.
8.2Las dos ramas
| Rama | Qué es | Qué mirar |
|---|---|---|
desordenado |
Todo apurado, sin criterio. Funciona igual de bien. | El server/index.js con todo adentro, y las dos tablas casi idénticas en el cliente. |
ordenado |
Lo mismo, con el criterio de la ayudantía. | Cómo quedó repartido: routes → controllers → services → db. |
8.3La comparación que vale la pena
# qué archivos cambiaron, y cuánto git diff desordenado ordenado --stat # el cambio completo de un archivo git diff desordenado ordenado -- server/index.js # saltar de una versión a la otra git switch desordenado git switch ordenado
La lógica es la misma en las dos ramas. Las mismas validaciones, la misma regla de choque de horario, las mismas respuestas HTTP. Lo único que cambió es dónde vive cada línea.
8.4Ejercicios, si quieren practicar
- Agrega un campo nuevoQue la reserva tenga un «motivo». Hazlo primero en la rama desordenada y después en la ordenada, y cuenta cuántos archivos tocaste en cada una.
- Agrega una reglaQue no se pueda reservar una mesa por más de cuatro horas seguidas. ¿Dónde va esa regla? Pista: es de la cocina.
- Cambia el color de todos los botonesEn la rama ordenada es un archivo. En la desordenada, anda a buscarlos.
- Agrega una tercera pantallaUn listado de usuarios. En la rama ordenada puedes reusar
DataTable; en la otra vas a terminar escribiendotabla3.jsx.