CicloUrbano funciona y está probada, pero vive en localhost, con una base de datos que es un fichero JSON y un servidor de desarrollo que jamás debería asomarse a internet. Esta lección cubre el último tramo: convertir el proyecto en un paquete de ficheros estáticos, entender qué se puede y qué no se puede esconder en una aplicación que se ejecuta en el navegador de otra persona, resolver el problema de enrutamiento que hace que /reservas devuelva un 404 en cuanto sale de localhost, desplegarlo, y saber qué vigilar después. Y como es la última lección del curso, cierra también el camino recorrido y señala por dónde seguir.

Contenido

  1. La construcción para producción
  2. Revisar el tamaño antes de publicar
  3. Variables de entorno: qué es realmente público
  4. El 404 al recargar: enrutamiento en cliente sobre un servidor estático
  5. Despliegue paso a paso en una plataforma estática
  6. La alternativa del contenedor: Docker y Nginx
  7. La API en producción: qué falta de verdad
  8. Después del despliegue: monitorización y métricas reales
  9. Lista de comprobación de lanzamiento
  10. Siguientes pasos con este proyecto
  11. Cómo seguir aprendiendo

  1. La construcción para producción

Hasta ahora todo ha ocurrido con npm run dev, donde Vite sirve los módulos sin empaquetar y recarga en caliente. Eso es cómodo para desarrollar y no es lo que se publica. La construcción es otra cosa:

npm run build
vite v6.0.5 building for production...
✓ 412 modules transformed.
dist/index.html                            0.48 kB │ gzip:  0.31 kB
dist/assets/index-B7dK2p9x.css            14.82 kB │ gzip:  3.41 kB
dist/assets/PaginaTaller-Qm3rT8vc.js       6.12 kB │ gzip:  2.18 kB
dist/assets/PaginaDetalleEstacion-Nx7.js   8.94 kB │ gzip:  3.02 kB
dist/assets/PaginaNuevaReserva-Zk1p.js    11.37 kB │ gzip:  3.86 kB
dist/assets/index-Yt4mR9Ka.js            268.41 kB │ gzip: 87.65 kB
✓ built in 3.42s

Qué ha hecho, paso a paso:

Tarea Qué implica
Empaquetado Cientos de módulos se combinan en unos pocos ficheros, para no hacer cientos de peticiones
Minificación Se eliminan espacios, comentarios y nombres largos de variables locales
Eliminación de código muerto Lo que no se importa desde ninguna parte no se incluye
División en fragmentos Cada ruta con lazy produce su propio fichero, como se preparó en División de Código
Modo producción de React Desaparecen los avisos de desarrollo, las comprobaciones extra y el doble render de StrictMode
Hash en el nombre index-Yt4mR9Ka.js contiene una huella del contenido

Ese hash es la pieza que más se malinterpreta. Sirve para poder decirle al servidor «guarda estos ficheros en caché para siempre» sin condenar al usuario a una versión antigua: si el contenido cambia, el nombre cambia, y el navegador lo pide como si fuera un fichero nuevo. La consecuencia práctica es una regla de caché de dos velocidades:

Fichero Cabecera de caché Por qué
index.html no-cache (o muy corta) Es el único que no lleva hash: es quien apunta a los demás
assets/* con hash max-age=31536000, immutable Su nombre cambia si cambia el contenido; cachearlo un año es seguro

Antes de publicar, se verifica la construcción tal cual, no el servidor de desarrollo:

npm run preview
# Sirve dist/ en http://localhost:4173

Es el momento de comprobar tres cosas que solo se ven aquí: que no hay errores en la consola sin los avisos de desarrollo, que los fragmentos perezosos se descargan al navegar (mira la pestaña de red) y que el Suspense de cada ruta muestra su esqueleto y no un salto en blanco.

  1. Revisar el tamaño antes de publicar

El informe de arriba ya da la cifra que importa: 87,65 kB comprimidos para el paquete principal. La revisión útil no es «¿es mucho?» en abstracto, sino comparar con el presupuesto que se fijó y localizar quién ocupa qué:

npx vite-bundle-visualizer

Un desglose típico de este proyecto:

Dependencia Aproximado (gzip) ¿Se puede reducir?
react + react-dom ~45 kB No: es el motor
react-router ~12 kB No, y ya divide por rutas
@reduxjs/toolkit + react-redux ~15 kB Solo eliminando Redux, que aquí se justificó
@tanstack/react-query ~13 kB No: sustituye a mucho código propio
Código de CicloUrbano ~3 kB Ya está dividido por rutas

La lección: en una aplicación bien dividida, el peso es casi todo dependencias, y la palanca que queda no es minificar mejor, sino elegir menos bibliotecas. Si el presupuesto se pasa, la conversación es sobre arquitectura, no sobre configuración.

  1. Variables de entorno: qué es realmente público

Vite expone al código solo las variables que empiezan por VITE_, y las sustituye en la construcción:

# .env.development
VITE_API_URL=http://localhost:3001
VITE_ENTORNO=desarrollo

# .env.production
VITE_API_URL=https://api.ciclourbano.example
VITE_ENTORNO=produccion
// src/configuracion.js — un único punto de lectura, como se decidió en 11-01
export const configuracion = {
  apiUrl: import.meta.env.VITE_API_URL,
  entorno: import.meta.env.VITE_ENTORNO,
  esProduccion: import.meta.env.PROD,
};

Y ahora la advertencia más importante de esta lección, porque es un error que se comete con frecuencia y tiene consecuencias reales:

Todo lo que pongas en import.meta.env acaba escrito, en texto legible, dentro de los ficheros JavaScript que descarga cualquier persona que visite la web. No es una variable «de servidor»: es una constante que se incrusta en el paquete. Cualquiera puede abrir las herramientas del navegador y leerla.

# Compruébalo tú mismo tras construir:
grep -r "api.ciclourbano.example" dist/assets/
# → aparece, tal cual, en el paquete

De ahí se deriva una frontera clara:

Puede ir en VITE_* No puede ir nunca
URL pública de la API Claves de API con permisos de escritura
Identificador público de un servicio de analítica Secretos de cliente de OAuth
Nombre del entorno, versión, banderas de funcionalidad Credenciales de base de datos
Clave publicable de una pasarela de pago Clave secreta de la misma pasarela

Lo que necesite un secreto necesita un servidor que lo guarde y haga la llamada por ti. No hay atajo. Y .env.production no se versiona: en el repositorio va .env.example con las claves y sin los valores, y los valores reales se configuran en la plataforma de despliegue.

  1. El 404 al recargar: enrutamiento en cliente sobre un servidor estático

Este es el fallo que sorprende a todo el mundo en su primer despliegue de una aplicación de una sola página. En local funciona; desplegado, entrar directamente en https://ciclourbano.example/reservas devuelve un 404.

La razón es que hay dos entidades distintas resolviendo rutas y solo una de ellas conoce las tuyas:

sequenceDiagram
    participant N as Navegador
    participant S as Servidor estático
    participant R as React Router
    Note over N,R: Navegación interna: funciona
    N->>R: clic en «Mis reservas»
    R->>N: pinta PaginaReservas y cambia la URL
    Note over N,R: Recarga o enlace directo: 404
    N->>S: GET /reservas
    S->>S: ¿existe el fichero /reservas?
    S--xN: 404 · React Router nunca llegó a ejecutarse

React Router vive dentro del JavaScript que carga index.html. Si el servidor no entrega index.html, no hay React, no hay Router y no hay ruta. La solución es siempre la misma idea —devolver index.html para cualquier ruta que no sea un fichero real— con una sintaxis distinta en cada plataforma:

Plataforma Fichero Contenido
Netlify public/_redirects /* /index.html 200
Vercel vercel.json Ver abajo
GitHub Pages public/404.html Una copia de index.html (no admite reescrituras)
Nginx Bloque server try_files $uri $uri/ /index.html;
Apache public/.htaccess Ver abajo
Cloudflare Pages public/_redirects Igual que Netlify
// vercel.json
{
  "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }],
  "headers": [
    {
      "source": "/assets/(.*)",
      "headers": [{ "key": "Cache-Control", "value": "public, max-age=31536000, immutable" }]
    }
  ]
}
# public/.htaccess
<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  # Si el fichero o el directorio existe, sírvelo tal cual
  RewriteCond %{REQUEST_FILENAME} -f [OR]
  RewriteCond %{REQUEST_FILENAME} -d
  RewriteRule ^ - [L]
  # Cualquier otra cosa: entrega index.html y deja que React Router decida
  RewriteRule ^ index.html [L]
</IfModule>

Fíjate en el detalle del código 200 en Netlify: es una reescritura, no una redirección. La URL que ve el usuario sigue siendo /reservas, que es justo lo que React Router necesita leer. Con un 301 o un 302 la URL cambiaría a / y el usuario acabaría en el catálogo.

Y la comprobación que nunca hay que saltarse tras desplegar: abrir una ruta profunda en una pestaña nueva y recargarla. Navegar hasta ella desde el inicio no prueba nada, porque ese camino nunca toca el servidor.

  1. Despliegue paso a paso en una plataforma estática

El proyecto son ficheros estáticos, así que casi cualquier alojamiento sirve. Con una plataforma conectada al repositorio, el despliegue queda automatizado:

Paso 1. Sube el proyecto a un repositorio remoto (GitHub, GitLab). El flujo de trabajo de CI escrito en la lección anterior empieza a ejecutarse en cada cambio.

Paso 2. Conecta el repositorio en la plataforma y configura la construcción:

Ajuste Valor
Comando de construcción npm run build
Directorio de publicación dist
Versión de Node 20 (la misma que en CI, para no descubrir diferencias tarde)
Comando de instalación npm ci

Paso 3. Añade las variables de entorno en el panel de la plataforma: VITE_API_URL con la URL real de la API y VITE_ENTORNO=produccion. Nunca en el repositorio.

Paso 4. Añade el fichero de reescritura de la sección anterior. Sin él, el despliegue «funciona» hasta que alguien comparte un enlace.

Paso 5. Despliega y comprueba, en este orden: la portada carga; una ruta profunda recargada en pestaña nueva funciona; el catálogo trae datos de la API real; una ruta inexistente muestra tu página de no encontrado; el panel de taller sigue vedado a un cliente; y la consola está limpia.

Paso 6. Activa los despliegues de vista previa por rama. Cada rama y cada solicitud de cambios obtiene su propia URL temporal, así que las revisiones dejan de ser «confía en mí» y pasan a ser «pruébalo aquí». Es, con diferencia, la función que más cambia la forma de trabajar en equipo.

  1. La alternativa del contenedor: Docker y Nginx

Si el despliegue va a la infraestructura propia de la empresa, la forma equivalente es una imagen que construye y sirve:

# Dockerfile
# --- Etapa 1: construir ---
FROM node:20-alpine AS construccion
WORKDIR /app
# Copiar primero los manifiestos aprovecha la caché de capas:
# si no cambian, no se reinstalan las dependencias
COPY package*.json ./
RUN npm ci
COPY . .
ARG VITE_API_URL
ENV VITE_API_URL=$VITE_API_URL
RUN npm run build

# --- Etapa 2: servir ---
# La imagen final no lleva Node ni node_modules: solo ficheros estáticos y Nginx
FROM nginx:alpine AS servicio
COPY --from=construccion /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# nginx.conf
server {
  listen 80;
  root /usr/share/nginx/html;

  gzip on;
  gzip_types text/css application/javascript application/json image/svg+xml;

  # Los ficheros con hash se pueden cachear indefinidamente
  location /assets/ {
    expires 1y;
    add_header Cache-Control "public, immutable";
  }

  # index.html nunca se cachea: es quien apunta a los assets nuevos
  location = /index.html {
    add_header Cache-Control "no-cache";
  }

  # La pieza clave: cualquier ruta desconocida entrega index.html
  location / {
    try_files $uri $uri/ /index.html;
  }
}

Dos detalles que conviene entender. El primero es que la construcción de dos etapas deja una imagen final de unos pocos megabytes: Node solo existe mientras se construye. El segundo es el ARG VITE_API_URL: como las variables se incrustan en la construcción, una imagen construida apunta a una API concreta. No puedes cambiarla al arrancar el contenedor. Si necesitas una imagen que sirva a varios entornos, la URL debe leerse en tiempo de ejecución (por ejemplo, de un /configuracion.json que se pide al arrancar la aplicación).

  1. La API en producción: qué falta de verdad

Hay que decirlo sin adornos: json-server es una herramienta de desarrollo. Acepta cualquier escritura de cualquiera, no tiene autenticación, no valida nada y guarda los datos en un fichero. Desplegarlo con datos reales sería un incidente de seguridad, no un despliegue.

Lo que hace falta detrás de una aplicación como esta:

Pieza Por qué Qué implica
API real Persistencia fiable, transacciones, integridad Un servicio propio (Node, Python, Java…) o una plataforma tipo backend as a service
Autenticación Saber quién pide algo Tokens de sesión en cookies httpOnly + Secure + SameSite; renovación y caducidad
Autorización en el servidor Que /taller sea de verdad solo del operario Comprobación de rol en cada endpoint, no solo en la interfaz
Validación en el servidor validarReserva es una comodidad, no una defensa Repetir las reglas en el servidor; es la única copia que cuenta
CORS Permitir a tu dominio y solo a él Cabeceras con la lista de orígenes permitidos
Límites de tasa Evitar abuso y coste descontrolado Cortafuegos de aplicación o límites por IP y por usuario
Bloqueo de sobrerreserva Dos personas reservando bici-001 a la vez Unicidad y transacción en la base de datos

Vale la pena releer con esta luz lo que se dijo en Rutas Protegidas: RutaProtegida y RequiereRol mejoran la experiencia y no protegen nada. Cualquiera puede abrir las herramientas del navegador, modificar el estado de la sesión y ver la pantalla del taller. Lo que impide que haga daño no es tu componente: es el servidor rechazando la petición.

Un último aviso de sensatez profesional: en cuanto haya datos de personas reales de por medio —nombres, correos, ubicaciones, pagos— el diseño de autenticación, almacenamiento y permisos debe revisarlo alguien con experiencia en seguridad y protección de datos antes de exponerlo al público. Todo el dominio de este curso es ficticio precisamente para poder aprender sin ese riesgo.

  1. Después del despliegue: monitorización y métricas reales

Publicar no es terminar. En producción hay navegadores, redes y personas que no estaban en tu portátil, y hay dos preguntas que solo se responden con datos de allí.

¿Está fallando algo? El registrarError de src/utilidades/monitorizacion.js lleva todo el curso siendo un console.error. Ahora es cuando conecta con un servicio de verdad:

// src/utilidades/monitorizacion.js
import { configuracion } from '../configuracion.js';

export function registrarError(error, pilaComponentes, contexto = {}) {
  if (!configuracion.esProduccion) {
    console.error('[CicloUrbano]', error, pilaComponentes, contexto);
    return;
  }

  // En producción se envía al servicio de monitorización.
  // Nunca incluyas datos personales en el informe: identificadores, no nombres ni correos.
  enviarInforme({
    mensaje: error.message,
    pila: error.stack,
    pilaComponentes,
    ruta: window.location.pathname,
    version: configuracion.version,
    usuarioId: contexto.usuarioId ?? null,
  });
}

Ese enganche ya está puesto donde tenía que estar: el componentDidCatch del LimiteDeError que se escribió en Límites de Error. El valor de haberlo previsto entonces se cobra ahora.

¿Va rápida para la gente de verdad? Las mediciones del Profiler se hicieron en un portátil con buena red. Las métricas de usuarios reales se recogen en el propio navegador y se envían:

// src/main.jsx
if (configuracion.esProduccion) {
  import('web-vitals').then(({ onLCP, onINP, onCLS }) => {
    onLCP(enviarMetrica);   // Cuánto tarda en pintarse lo principal
    onINP(enviarMetrica);   // Cuánto tarda en responder a una interacción
    onCLS(enviarMetrica);   // Cuánto se mueve el contenido mientras carga
  });
}

Y hace falta una tercera cosa que no es técnica: un canal de vuelta. Un enlace visible para informar de un problema, y alguien que lo lea. La mayoría de los fallos que importan los cuenta una persona antes de que ninguna métrica los delate.

  1. Lista de comprobación de lanzamiento

Área Comprobación
Construcción npm run build sin avisos · npm run preview recorrido completo · tamaño dentro del presupuesto
Enrutamiento Ruta profunda recargada en pestaña nueva · ruta inexistente muestra tu 404 · /taller vedado a un cliente
Rendimiento LCP y INP medidos en la construcción de producción · fragmentos perezosos descargándose al navegar
Accesibilidad Recorrido completo con Tab · foco visible · formularios etiquetados · contraste suficiente en ambos temas · auditoría del navegador
SEO básico <title> y descripción · idioma en <html lang="es"> · favicon · robots.txt
Errores registrarError conectado · LimiteDeError con un mensaje humano · ningún dato personal en los informes
Seguridad Ningún secreto en el paquete (grep sobre dist/) · HTTPS · cabeceras de seguridad · autorización comprobada en el servidor
Datos Copias de seguridad de la base de datos · plan de restauración probado
Operación Analítica respetuosa con la privacidad · alertas configuradas · plan de reversión al despliegue anterior
Personas Canal para informar de fallos · alguien de guardia el día del lanzamiento

El punto que más se olvida y más duele es el plan de reversión. Antes de publicar, hay que saber cómo se vuelve a la versión anterior y cuánto tarda. Las plataformas conectadas al repositorio lo dan hecho con un botón; con contenedores, es volver a desplegar la etiqueta anterior. Averígualo antes de necesitarlo.

  1. Siguientes pasos con este proyecto

CicloUrbano está viva y tiene una hoja de ruta natural, toda ella apoyada en lo que ya sabes:

Mejora Con qué del curso Qué gana
Migrar a TypeScript TypeScript con React, fichero a fichero empezando por src/tipos/dominio.ts Contratos explícitos y refactorizaciones seguras; menos pruebas triviales
Escaparate público SSR y SSG con Next.js para catálogo y fichas Contenido indexable y primer pintado inmediato, junto a la gestión en la SPA
Aplicación móvil React Native con Expo, reutilizando validaciones, hooks y slices Presencia nativa con la mayor parte de la lógica compartida
Internacionalización Extraer los textos y añadir catalán e inglés Alcance real de la aplicación
Modo sin conexión Un service worker y la persistencia de la caché de Query Una app usable en la calle con mala cobertura
Pagos y facturación Una pasarela real, con la clave secreta en el servidor El paso de prototipo a producto
Panel de métricas El componente de gráficas ya aislado en un fragmento perezoso Decisiones de negocio con datos

El mejor consejo para elegir es el mismo que ordenó el proyecto desde el principio: incrementos verticales. Una mejora completa, de la interfaz a la prueba, antes de empezar la siguiente.

  1. Cómo seguir aprendiendo

  • La documentación oficial de React es, hoy, excelente. Sus páginas sobre «no necesitas un efecto» y sobre cómo estructurar el estado explican mejor que ningún tutorial por qué las cosas son como son. Vuelve a ellas con la experiencia que ya tienes: se leen distintas.
  • Lee el código de las bibliotecas que usas. React Router, Redux Toolkit y TanStack Query son legibles y están bien escritas. Entender cómo resuelven un problema enseña más que cualquier curso, incluido este.
  • Escribe tu propia versión reducida de algo que uses: un useQuery mínimo con caché, un enrutador de cincuenta líneas. Nada consolida un modelo mental como reimplementarlo.
  • Reproduce los fallos que te encuentres en un ejemplo mínimo antes de pedir ayuda. La mitad de las veces se resuelven en el intento, y la otra mitad recibes una respuesta mucho mejor.
  • Contribuye. Empieza por documentación y por reproducir incidencias; es la puerta de entrada más honesta a un proyecto de código abierto.
  • Mantén el escepticismo con lo nuevo. El ecosistema de React se mueve rápido y no todo lo que aparece sobrevive. La forma de distinguir la moda de la mejora es preguntarse qué problema concreto resuelve y si tú lo tienes.

Errores Comunes y Consejos

  • Desplegar sin la regla de reescritura. La aplicación parece funcionar hasta que alguien recarga o comparte un enlace. Comprueba siempre una ruta profunda en pestaña nueva.
  • Poner un secreto en una variable VITE_. Queda en texto legible en el paquete. Si algo debe ser secreto, necesita un servidor.
  • Cachear index.html durante mucho tiempo. Los usuarios se quedan clavados en la versión anterior aunque los assets nuevos ya estén publicados. index.html sin caché, assets con hash y caché larga.
  • Confiar en RutaProtegida como seguridad. Es experiencia de usuario. La autorización se comprueba en el servidor, en cada endpoint, siempre.
  • Probar el rendimiento en el servidor de desarrollo. Los números no se parecen a los de producción. Mide sobre npm run preview o sobre el despliegue real.
  • Desplegar un viernes por la tarde sin plan de reversión. No es una broma: si no sabes cómo volver atrás y cuánto tarda, no estás listo para publicar.
  • Consejo final: automatiza el despliegue el mismo día que hagas el primero a mano. Un despliegue que requiere recordar pasos es un despliegue que alguien hará mal.

Ejercicios

Ejercicio 1. Despliega CicloUrbano en una plataforma estática con la reescritura correcta y las cabeceras de caché de la sección 1. Documenta en el README.md el procedimiento y el plan de reversión, y verifica con grep sobre dist/ que no se ha colado ningún secreto.

Ejercicio 2. Prepara el proyecto para servirse desde una subruta (https://ejemplo.example/ciclourbano/) en lugar de la raíz del dominio. Averigua qué hay que cambiar en Vite, en React Router y en la configuración del servidor.

Ejercicio 3. Añade al flujo de integración continua un paso que falle la construcción si el paquete principal supera un presupuesto de 100 kB comprimidos, para que el tamaño no se degrade sin que nadie se entere.

Soluciones

Solución 1.

proyecto/
├── public/_redirects        →  /*    /index.html   200
└── vercel.json (si es Vercel, con rewrites y headers)
# Verificación previa obligatoria: ningún secreto en el paquete
npm run build
grep -rEi "secret|password|sk_live|private_key" dist/assets/ && echo "REVISAR" || echo "limpio"

# Comprobación posterior al despliegue
curl -I https://ciclourbano.example/reservas          # debe devolver 200 y text/html
curl -I https://ciclourbano.example/assets/index-*.js # debe traer Cache-Control: immutable

En el README.md:

## Despliegue

Automático al integrar en `main`. La plataforma ejecuta `npm ci` y `npm run build`
y publica `dist/`. Variables necesarias: `VITE_API_URL`, `VITE_ENTORNO`.

### Comprobación tras desplegar
1. Abrir `/reservas` en una pestaña nueva y recargar → debe cargar (no 404).
2. Ruta inexistente → página propia de no encontrado.
3. `/taller` con sesión de cliente → «sin permisos».
4. Consola del navegador sin errores.

### Reversión
Panel de despliegues → seleccionar el anterior correcto → «Restaurar».
Tiempo estimado: menos de 2 minutos. No requiere reconstruir.

Solución 2. Hay que tocar tres sitios, y olvidarse de cualquiera de ellos produce una pantalla en blanco o enlaces rotos:

// 1. vite.config.js — prefijo de las rutas de los assets en index.html
export default defineConfig({
  base: '/ciclourbano/',
  plugins: [react()],
});
// 2. src/rutas.jsx — React Router debe ignorar el prefijo al comparar rutas
export const router = createBrowserRouter(rutas, {
  basename: '/ciclourbano',
});
# 3. Servidor — la reescritura debe apuntar al index.html de la subruta
location /ciclourbano/ {
  alias /usr/share/nginx/html/;
  try_files $uri $uri/ /ciclourbano/index.html;
}

Comprobación: dist/index.html debe referenciar /ciclourbano/assets/..., y una recarga en /ciclourbano/reservas debe funcionar. Los enlaces internos con <Link to="/reservas"> siguen escribiéndose sin el prefijo: lo añade el basename.

Solución 3.

// package.json
{
  "scripts": {
    "presupuesto": "node scripts/comprobar-presupuesto.js"
  }
}
// scripts/comprobar-presupuesto.js
import { readdirSync, readFileSync } from 'node:fs';
import { gzipSync } from 'node:zlib';
import { join } from 'node:path';

const LIMITE_KB = 100;
const CARPETA = 'dist/assets';

// El paquete principal es el que empieza por index- y acaba en .js
const principal = readdirSync(CARPETA).find((f) => /^index-.*\.js$/.test(f));

if (!principal) {
  console.error('No se ha encontrado el paquete principal. ¿Se ha ejecutado npm run build?');
  process.exit(1);
}

const bytes = gzipSync(readFileSync(join(CARPETA, principal))).length;
const kb = bytes / 1024;

console.log(`${principal}: ${kb.toFixed(2)} kB comprimidos (límite ${LIMITE_KB} kB)`);

if (kb > LIMITE_KB) {
  console.error(
    `\nPresupuesto superado en ${(kb - LIMITE_KB).toFixed(2)} kB.\n` +
    'Revisa las dependencias nuevas con: npx vite-bundle-visualizer'
  );
  process.exit(1);   // El código de salida distinto de cero es lo que rompe la construcción en CI
}
# .github/workflows/pruebas.yml — añadido al trabajo de extremo a extremo
      - run: npm run build
      - run: npm run presupuesto     # Falla el trabajo si el paquete ha engordado

La clave es el process.exit(1): sin él, el script informa pero la integración continua sigue en verde y el presupuesto se convierte en una sugerencia que nadie atiende.

Conclusión

Empezaste este curso con un <div id="root"> vacío y la pregunta de por qué alguien necesitaría una biblioteca para pintar una lista de bicicletas. Acabas con una aplicación completa, probada en cuatro niveles, medida, dividida en fragmentos, accesible y desplegada, y con criterio propio para decidir cómo construir la siguiente.

El camino ha sido este. En los fundamentos entendiste que React es declarativo y que la interfaz es una función del estado, y viste qué ocurre de verdad entre un cambio de estado y un píxel en la pantalla. Con los componentes, las props y el estado aprendiste a delimitar piezas y a parametrizarlas, y con los eventos, los formularios y la accesibilidad hiciste que respondieran a personas, no solo a datos. En los conceptos avanzados apareció la disciplina: elevar el estado al ancestro común, componer en lugar de heredar, y capturar los fallos antes de que se coman la pantalla. Los hooks te dieron el vocabulario completo —estado, sincronización con el exterior, referencias, contexto, reductores— y, sobre todo, la capacidad de extraer tu propia lógica reutilizable. React Router convirtió una pantalla en una aplicación con URLs compartibles y accesos controlados. La gestión del estado puso orden en el caos con una idea que vale más que cualquier biblioteca: cada dato tiene un sitio donde debe vivir, y los datos del servidor no son tuyos. El rendimiento te enseñó a medir antes de tocar y a no confundir optimización con decoración. Las pruebas te dieron la libertad de refactorizar sin miedo, probando comportamiento y no implementación. Los temas avanzados ampliaron el mapa hasta el servidor, los tipos y el móvil. Y el proyecto demostró que todo eso encaja.

Lo que te llevas no es una lista de APIs, que cambiarán. Es la forma de pensar que hay debajo: describir la interfaz en función del estado, poner cada dato en su sitio, medir antes de optimizar, probar el comportamiento visible, y no confiar nunca en el cliente para lo que debe garantizar el servidor. Eso sigue siendo cierto cuando cambia la versión, la biblioteca de moda o el framework entero.

Ya tienes lo que hace falta. Construye algo.

Curso de React

Módulo 1: Introducción a React

Módulo 2: Componentes de React

Módulo 3: Trabajando con Eventos

Módulo 4: Conceptos Avanzados de Componentes

Módulo 5: Hooks de React

Módulo 6: Enrutamiento en React

Módulo 7: Gestión del Estado

Módulo 8: Optimización del Rendimiento

Módulo 9: Pruebas en React

Módulo 10: Temas Avanzados

Módulo 11: Proyecto: Construyendo una Aplicación Completa

© Copyright 2026. Todos los derechos reservados