Ayudantía
06

Organización de proyecto, componentes y librerías UI

Material de apoyo · léelo con el repositorio de ejemplo abierto al lado

Ingeniería de Software 2026-2 Grupo 3 · Vicente Aedo · Nelson Avello · Fabián Cartes
La idea completa

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 restauranteEn el proyectoSu regla
La cartaroutes/Dice qué se puede pedir. No prepara nada.
El anfitriónmiddlewares/Dice «pasa» o «no pasa». Nunca cocina.
El meserocontrollers/Lleva y trae. No decide la receta.
La cocinaservices/Tiene las recetas. Nunca atiende mesas.
La bodegaprisma/Guarda y entrega. No decide nada.
El comedorclient/Todo lo que el cliente ve y toca.
Los salonespages/Cada uno es una pantalla con su dirección.
La lozacomponents/Las piezas que se repiten en todas las mesas.
El mesónservices/ (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.

Parada 1 de 5

1.2La prueba de fuego

Hay una sola pregunta que decide si una regla está en el lugar correcto:

La pregunta

¿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?

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.

Sirve para cualquier regla

«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

01

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.

02

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.

03

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.

04

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.

El mapa

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

Elige una carpeta o un archivo del árbol.
El concepto que más se atraganta

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.

En una frase

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:

javascript · una función cualquiera
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:

components/ui/Boton.jsx
function Boton({ texto }) {
  return <button>{texto}</button>
}

<Boton texto="Guardar" />
<Boton texto="Cancelar" />

3.2Puente dos: es un atributo de HTML

Llevas años escribiendo esto sin pensarlo:

html · algo que ya escribiste mil veces
<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.

No te quedes ahí

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.

components/ui/Boton.jsx — en vivo

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 componente avisa, no decide
// 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

La regla

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.

El error que vas a cometer

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.

  1. Tengo un botón que dice «Guardar» y necesito uno que diga «Cancelar». ¿Qué tiene que entrar por props?El texto.
  2. ¿Y si además uno es rojo y el otro gris?El color también. Dos props: el texto y la variante.
  3. ¿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.
Lo que en la ayudantía nos saltamos

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:

El test

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

NivelQué esEjemplosCó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

Una señal práctica

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.

Así no controllers/reserva.controller.js
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)
}
Cuatro trabajos en un archivo

El peor es el segundo. Toca las anotaciones de arriba para ver dónde está cada uno.

Así sí controllers/reserva.controller.js — el mesero
export const crear = async (req, res, next) => {
  try {
    const r = await reservaService.crear(req.body)
    res.status(201).json(r)
  } catch (e) { next(e) }
}
Así sí services/reserva.service.js — la cocina
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 } })
}
Así sí middlewares/ — el anfitrión y el manejador de errores
// 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 })
}
Lo que se ganó

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

routes/reserva.routes.js — la carta
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 e index.js — armar y abrir son dos cosas distintas
// 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.

Así no en cada archivo que necesita la base
const prisma = new PrismaClient()   // ← y otro, y otro, y otro…
Así sí config/prisma.js — se crea una vez
import { PrismaClient } from '@prisma/client'
export const prisma = new PrismaClient()

// y en todos los demás archivos:
import { prisma } from '../config/prisma.js'
Qué pasa si no lo hacen

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

Así no la dirección escrita a mano, en doce pantallas
useEffect(() => {
  fetch('http://localhost:3000/api/reservas', {
    headers: { Authorization: 'Bearer ' + token }
  }).then(r => r.json()).then(setReservas)
}, [])
Así sí services/api.js — el único archivo que conoce la dirección
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)
})
services/reservaService.js — un archivo por recurso
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)
Y el .env

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.

Ojo con Vite

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

Así no el botón que decide todo por su cuenta
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.
Así sí components/ui/Boton.jsx
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>
  )
}
y así se usa, en cualquier pantalla
<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

components/ui/Card.jsx
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>
  )
}
la misma tarjeta, dos contenidos distintos
<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

components/DataTable.jsx
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>
  )
}
pages/ReservasPage.jsx — la pantalla decide qué columnas quiere
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" />
El render es el truco

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

hooks/useFetch.js
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, []))

Cuándo hacer un hook

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

components/ui/Boton.jsx — el único archivo que conoce MUI
import { Button } from '@mui/material'

export default function Boton({ children, ...props }) {
  return (
    <Button variant="contained" disableElevation {...props}>
      {children}
    </Button>
  )
}
y si mañana cambian a ShadCN, solo cambia este archivo
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
Qué compra esto

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.

La decisión que hay que tomar en grupo

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.

Hecho a mano en veinte minutos

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.
El mismo modal, de una librería

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" y aria-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.
Y no es solo el modal

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

Familia 1

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.
Familia 2

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.
Una aclaración que decide bastante

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.

Familia 1 · queda fuera del repositorio
terminal
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.

Familia 2 · queda dentro del repositorio
terminal
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 preguntaFamilia 1 · dependenciaFamilia 2 · código copiado
¿Qué tan rápido necesitan partir?Muy rápido: llega estiladoMás lento: hay que estilar todo
¿Cuánto van a personalizar?Hasta donde el tema los dejeSin techo: el archivo es suyo
¿El grupo usa Tailwind?No hace faltaObligatorio, no es opcional
¿Quién mantiene el componente?La librería, y actualizan con npmUstedes, de acá hasta diciembre
¿Cuánto pesa en el navegador?Suma la librería completaSolo lo que copiaron
¿Qué pasa al actualizar?npm update y revisar que no rompaCopiar de nuevo y comparar a mano
Elijan una y anótenla

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.

Qué compra esto

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.

Lo que no alcanzamos a ver

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.

Opción A

Por tipo

Todo lo del mismo tipo junto. Es lo que vimos en la ayudantía.

src/
├── components/
├── pages/
├── hooks/
├── services/
└── utils/
Opción B

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 tipoPor módulo
Tamaño del proyectoHasta unas 15 o 20 pantallasDe ahí en adelante
Cómo se reparte el trabajoTodos tocan todoCada integrante es dueño de un módulo
Buscar un archivoSabes el tipo, vas a su carpetaSabes el módulo, vas a su carpeta
Conflictos en GitMás probables: todos editan components/Menos probables: cada uno en lo suyo
Lo difícilcomponents/ se llena y hay que reordenarDecidir qué es compartido y qué no
Curva de entradaSe entiende solaHay que explicarla
Para el proyecto del ramo

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 síntoma

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.

Regla práctica

Algo se mueve a shared/ cuando lo necesita el segundo módulo, no antes. Otra vez la misma idea: no abstraigas antes de tiempo.

Sin que se caiga

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.

Antes de empezar

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

  1. Rama aparte, siempre Nunca directo a dev y muchísimo menos a main. Si algo sale mal, se borra la rama y no pasó nada.
    git switch -c refactor/estructura
  2. 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
  3. 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/"
  4. 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.
  5. 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:

  1. Primero el backend, que es más fácilSaca las consultas del controlador a un service. Un recurso a la vez.
  2. 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.
  3. Después mueve las pantallas a pages/Es solo mover archivos y arreglar imports.
  4. Al final, extrae los componentes repetidosEsto es lo único que requiere pensar, y por eso va último, cuando el resto ya está ordenado.
Por qué ese orden

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

Para abrirlo hoy mismo

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.

dos terminales
# 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
Una aclaración importante

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

RamaQué esQué 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

git
# 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
Lo que van a ver

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

  1. 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.
  2. 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.
  3. Cambia el color de todos los botonesEn la rama ordenada es un archivo. En la desordenada, anda a buscarlos.
  4. Agrega una tercera pantallaUn listado de usuarios. En la rama ordenada puedes reusar DataTable; en la otra vas a terminar escribiendo tabla3.jsx.