Localhost no es entorno de producción: diagnóstico de aplicaciones React que fallan al desplegarse
Aprenda por qué una aplicación React que funciona bien en su máquina falla una vez desplegada, y cómo rastrear las URLs de API, variables de entorno, CORS, enrutamiento, recursos y autenticación hasta la capa que causa el fallo.
Fuentes: Why Your React App Works Locally but Breaks After Deployment: 10 Common Problems and How to Fix Them
Una aplicación React que funciona perfectamente en localhost pero deja de funcionar en cuanto se despliega es una de las frustraciones más comunes en el trabajo frontend. Las solicitudes dejan de devolver datos, al actualizar la página se obtiene un 404, las imágenes desaparecen, el inicio de sesión falla silenciosamente, o la plataforma de hosting se niega a compilar código que funcionaba bien en tu portátil. Casi ninguno de estos problemas proviene de React en sí; surgen debido a la diferencia entre el entorno en el que desarrollas y el que realmente utilizan tus usuarios. Esta guía explica las diez causas más comunes, muestra cómo se manifiestan cada una y finaliza con un método de depuración y una lista de verificación antes del despliegue que puedes reutilizar en cada lanzamiento.
Por qué el mismo código se comporta de manera diferente en producción
Un frontend de React rara vez es toda la aplicación. Él se comunica con una API, la API se comunica con una base de datos, y cada eslabón de esa cadena depende de la configuración: variables de entorno, nombres de dominio, reglas de red, ajustes de autenticación y todo lo que la plataforma de hosting hace con las solicitudes recibidas. En su máquina, usted controla todo eso de forma implícita. Sabe en qué puerto escucha el backend, qué archivo .env se carga y qué paquetes se encuentran en node_modules. En producción, cada una de esas suposiciones se reemplaza por algo explícito, y cualquier discrepancia puede hacer que el código correcto parezca defectuoso.
Tenga este enfoque en mente al leer las secciones siguientes. La mayoría de las soluciones son cambios de configuración, no cambios en el código.
1. La URL de la API sigue apuntando a localhost
Cuando una aplicación desplegada no muestra datos, lo primero que vale la pena verificar es a dónde se dirigen sus solicitudes. Durante el desarrollo es normal ejecutar el backend localmente e incluir su dirección de forma fija en el frontend:
const API_URL = "http://localhost:8080/api";
Eso funciona porque tu navegador y tu backend se ejecutan en la misma computadora. Una vez que el frontend se despliega en una dirección como https://my-app.example.com, la misma línea sigue enviando solicitudes a http://localhost:8080/api. El problema es que localhost es resuelto por el navegador que ejecuta el código. Para un visitante, eso significa su propia máquina, no tu equipo de desarrollo. La ruta de la solicitud pasa a ser, en efecto:
User's Browser
↓
localhost:8080
↓
User's computer
A menos que ese visitante tenga tu backend ejecutándose en el puerto 8080, todas las solicitudes fallarán.
La solución es dejar de codificar la dirección de forma fija y leerla desde una variable de entorno. En un proyecto Vite, se declara el valor en un archivo env:
VITE_API_URL=https://your-backend.example.com/api
y se lee en el código mediante import.meta.env:
const API_URL = import.meta.env.VITE_API_URL;
Ahora el entorno de desarrollo puede apuntar a http://localhost:8080/api mientras que la versión de producción apunta a algo como https://api.example.com/api, sin necesidad de cambiar el código en ninguno de los casos. Se trata de una pequeña refactorización, pero elimina uno de los errores más comunes en el despliegue: pedirle al navegador de un tercero que se comunique con tu ordenador portátil.
Otra medida de protección relacionada es generar un error audible cuando la variable está ausente. Si API_URL resulta ser undefined, las solicitudes se dirigen a rutas como undefined/users, lo cual dificulta su diagnóstico. Una verificación de una sola línea al iniciar el programa que registre un mensaje o genere un error cuando falte el valor ahorra tiempo posteriormente.
2. Las variables de entorno del entorno de producción nunca se configuraron
Las variables de entorno suelen ser la siguiente causa probable. Localmente, probablemente tengas un archivo .env que contiene todo lo necesario para la aplicación:
VITE_API_URL=http://localhost:8080/api
VITE_APP_NAME=MyApp
Dichos valores solo existen en tu ordenador. Una plataforma de hosting no lee tu archivo local .env (y no debería hacerlo, ya que ese archivo suele excluirse del control de versiones). La versión final para producción debe recibir sus propios valores, configurados en la sección de variables de entorno del proveedor:
VITE_API_URL=https://your-backend.example.com/api
Después de agregar o cambiar un valor allí, es necesario iniciar una nueva implementación. Con Vite, este paso no es opcional: solo las variables precedidas por VITE_ se exponen al código del cliente, y Vite las sustituye en el paquete durante la compilación. Cambiar una variable en el panel de control no tiene efecto alguno en un paquete que ya se haya compilado con el valor antiguo.
Las variables del frontend no son secretos
Hay una lección de seguridad oculta en esto. Colocar un valor en .env no lo convierte en algo privado. Todo lo que se expone al cliente se compila en JavaScript que cada visitante descarga y puede leer. Las contraseñas de la base de datos, las claves API privadas y otras credenciales del lado del servidor nunca deben encontrarse en variables VITE_*. Guárdelas en el backend, o detrás de una función del lado del servidor, y permita que el frontend llame a un endpoint que las utilice en su nombre.
3. Las reglas CORS solo permiten el origen de desarrollo
Los errores de Cross-Origin Resource Sharing son otra sorpresa clásica después del despliegue. Imagine un frontend servido desde:
https://myapp.example.com
y un backend servido desde:
https://api.example.com
Estos son orígenes diferentes, ya que un origen es la combinación de esquema, host y puerto. Los navegadores aplican la política de mismo origen, por lo que para que el frontend pueda leer las respuestas de la API, el backend debe responder con encabezados CORS que permitan explícitamente el origen del frontend.
Durante el desarrollo, es probable que el backend estuviera configurado para aceptar solicitudes desde el servidor de desarrollo de Vite:
http://localhost:5173
Todo funcionaba, por lo que es fácil asumir que el CORS está configurado. Sin embargo, después del despliegue, el origen del frontend es:
https://myapp.example.com
Para el navegador, se trata de un origen completamente nuevo. Si el backend sigue listando únicamente http://localhost:5173, el frontend desplegado queda bloqueado. En el caso de un backend Spring Boot, permitir el origen de producción en un controlador podría verse así (el fragmento es en Java):
@CrossOrigin(origins = "https://myapp.example.com")
@RestController
public class UserController {
// ...
}
Anotar cada controlador funciona para servicios pequeños, pero en una base de código más grande es más fácil mantener una única configuración central de CORS y más difícil olvidarla al agregar endpoints. Sea cual sea la tecnología utilizada, la regla es la misma: los orígenes permitidos deben coincidir exactamente con el origen real del frontend de producción, incluyendo https y cualquier subdominio. Un patrón común y funcional es leer la lista de orígenes permitidos desde la configuración de entorno del propio backend, de modo que el desarrollo, las pruebas y la producción permitan acceder al frontend adecuado.
Tenga en cuenta que CORS es un mecanismo del navegador. Una solicitud que falla con un error CORS en el navegador puede tener éxito desde curl o Postman, y por eso probar la API fuera del navegador puede dar una sensación engañosa de que todo está bien.
4. Al actualizar una ruta del lado del cliente se obtiene 404
Esto suele hacer que los desarrolladores examinen detenidamente el código de su enrutador cuando este funciona correctamente. Supongamos que la aplicación tiene una ruta como esta:
/dashboard
Llega a la página de inicio, hace clic en un enlace hacia el panel de control y este se muestra perfectamente. Puede navegar sin problemas. Luego pulsa actualizar y el servidor responde con:
404 Not Found
La explicación radica en cómo funcionan las aplicaciones de una sola página. Cuando hace clic en un enlace, React Router actualiza la URL y renderiza completamente el componente correspondiente en el navegador; nunca se envía una solicitud a /dashboard al servidor. Cuando actualiza la página o abre esa URL directamente, el navegador solicita /dashboard al servidor de alojamiento. Si el servidor solo conoce archivos reales, busca un recurso con esa ruta, no encuentra nada y devuelve un 404.
La solución es una regla de respaldo en el host: toda solicitud a una ruta del frontend que no coincida con un archivo real debe ser reescrita o redirigida a index.html. Luego el navegador carga la aplicación, y React Router lee la URL y muestra la pantalla adecuada. La documentación de despliegue de React Router describe este requisito, y la mayoría de los hosts estáticos ofrecen una configuración o un pequeño archivo de configuración para ello. Así que, cuando hacer clic funciona pero actualizar falla, déjese las rutas como están y primero revise la configuración de reescritura de SPA del host. Si desea repasar cómo se declaran las rutas, consulte Conceptos básicos de React Router.
5. Rutas de importación con mayúsculas y minúsculas incorrectas
Algunos errores permanecen invisibles hasta que la compilación se ejecuta en un sistema operativo diferente. Imagine un archivo de componente llamado:
Header.jsx
Se importa de esta manera:
import Header from "./components/header";
La única diferencia es una letra mayúscula, que es fácil pasar por alto durante las revisiones. Los sistemas de archivos difieren en cuanto a si eso tiene importancia. Las configuraciones predeterminadas en macOS y Windows son insensibles a mayúsculas y minúsculas, por lo que la importación se realiza y la aplicación funciona. La mayoría de los servidores de compilación y contenedores ejecutan Linux, cuyos sistemas de archivos son sensibles a mayúsculas y minúsculas; por eso Header.jsx y header.jsx son dos nombres no relacionados y la compilación falla con un error de módulo no encontrado.
La solución es simplemente hacer que cada importación coincida carácter por carácter con el archivo en el disco. Dado:
components/Header.jsx
Escriba la importación de la siguiente manera:
import Header from "./components/Header";
Debe prestarse la misma atención a los nombres de las carpetas. Una trampa adicional que cabe conocer: renombrar un archivo únicamente por cuestión de mayúsculas y minúsculas podría no ser detectado por Git en un sistema de archivos insensible a mayúsculas y minúsculas, por lo que el repositorio podría seguir conservando el nombre antiguo aunque su editor muestre el nuevo. Verificar el nombre que realmente registra Git resuelve el misterio rápidamente.
6. Las imágenes y los recursos estáticos desaparecen
A veces la aplicación en sí se carga, pero sus imágenes, fuentes o íconos no. Eso suele significar que los recursos se hacen referencia mediante rutas que funcionaban en el servidor de desarrollo pero que no existen en la versión final para producción.
Vite ofrece dos mecanismos distintos, y es útil saber cuál se está utilizando. El primero consiste en importar un recurso desde el código fuente:
import logo from "./assets/logo.png";
y utilizar el valor importado como fuente de la imagen:
function App() {
return <img src={logo} alt="Logo" />;
}
Dado que el recurso pasa por el grafo de módulos, Vite lo procesa durante la compilación, lo copia en el resultado con un nombre de archivo generado y le proporciona la URL correcta. Nunca tiene que adivinar la ruta final.
El segundo mecanismo es el directorio public, para archivos que deben servirse tal como están sin procesamiento alguno:
public/
└── logo.png
Los archivos de allí se copian a la raíz del resultado de la compilación, por lo que se hace referencia a ellos mediante una ruta URL absoluta:
<img src="/logo.png" alt="Logo" />
El principio subyacente es la diferencia entre una ruta en su disco y una URL que el navegador puede obtener. Su editor puede saber exactamente dónde se encuentra src/assets/logo.png, pero el navegador solo entiende las URLs que realmente sirve el servidor desplegado. Si la aplicación está alojada bajo una subruta en lugar de en la raíz del dominio, las rutas absolutas como /logo.png también pueden fallar, y para eso existe la opción base de Vite.
Cuando falta un recurso, abra la pestaña Network del navegador e inspeccione la solicitud fallida. Por lo general, verá una ruta que no existe en el build o un error 404 del servidor, lo cual le indicará cuál de los dos mecanismos necesita corrección.
7. El servidor de desarrollo no es la versión de producción
Ejecutando la aplicación con:
npm run dev
inicia un servidor de desarrollo con reemplazo en tiempo real de módulos, advertencias detalladas y transformación a demanda de tus módulos. Lo que se despliega es algo distinto: un conjunto optimizado de archivos estáticos empaquetados. Para un proyecto Vite, lo generas con:
npm run build
y tu servidor sirve el resultado obtenido. Dado que los dos métodos procesan tu código de manera diferente, es totalmente posible que una aplicación funcione bajo:
npm run dev
pero falle bajo:
npm run build
Ese fallo es una buena noticia si lo detectas tú mismo, ya que puedes solucionarlo antes de que algo llegue a los usuarios. Vite también te permite servir el resultado generado localmente con:
npm run preview
Esto está mucho más cerca de lo que recibirán los visitantes que el servidor de desarrollo. Haz que la creación y visualización local sean un hábito antes de cada despliegue, en lugar de dejar que la plataforma de hosting sea el primer lugar donde se ejecute la versión final del proyecto. Al integrar npm run build en CI, obtendrás la misma garantía de forma automática en cada solicitud de integración.
8. Tus node_modules locales ocultan dependencias faltantes
Un directorio node_modules con muchos archivos puede hacer que un proyecto parezca estar en mejor estado de lo que realmente está. A lo largo de las semanas de desarrollo, se acumulan allí paquetes: cosas que instalaste una vez para probarlas, dependencias transitivas de otros paquetes y restos de ramas que abandonaste. Tu código podría importar uno de ellos y funcionar simplemente porque sucede que está presente.
Un entorno de despliegue comienza limpio. Instala exactamente lo que describen package.json y el archivo de bloqueo, nada más. Por lo tanto, si su código contiene:
import axios from "axios";
entonces Axios debe declararse como una dependencia real del proyecto, por ejemplo:
{
"dependencies": {
"axios": "^1.0.0",
"react": "^19.0.0",
"react-router-dom": "^7.0.0"
}
}
Los números de versión en ese fragmento son ilustrativos; utilice los que realmente necesita su proyecto. El objetivo es que un clon nuevo seguido de una instalación nueva contenga todo lo necesario para compilar y ejecutar la aplicación. Guarde tanto package.json como el archivo de bloqueo y manténgalos sincronizados, de modo que cada instalación utilice las mismas versiones.
Para reproducir localmente las condiciones de despliegue, elimine node_modules e instálelo de nuevo, idealmente con npm ci, que realiza la instalación estrictamente a partir del archivo lockfile y falla si este y package.json no coinciden. Si la compilación falla después de eso, habrá detectado una dependencia no declarada antes que su servidor.
9. El frontend funciona bien pero el backend falla
Es tentador culpar a React cada vez que la pantalla no se muestra correctamente, pero el problema suele estar más abajo en la cadena de procesamiento. Supongamos que el frontend desplegado se carga con éxito en:
https://myapp.example.com
Abre el panel de control y está vacío. En la pestaña Network encuentra:
GET https://api.example.com/api/dashboard
500 Internal Server Error
Esa línea revela mucha información. La aplicación React cumplió con su función y envió la solicitud; el backend la recibió pero falló al procesarla. Las causas más probables incluyen una conexión a la base de datos rota, una variable de entorno del lado servidor faltante, un problema de autenticación, una excepción no manejada o una configuración diferente a la de tu entorno local.
Una solicitud típica full-stack atraviesa estos niveles:
React Frontend
↓
REST API
↓
Backend
↓
Database
Un fallo en la capa inferior se refleja en la superior. Si no es posible acceder a la base de datos, el panel de control parece defectuoso aunque cada línea de código del frontend esté correcta. Por eso el diagnóstico no debe limitarse al navegador. Cuando falla una llamada a la API, revisa la pestaña Network y luego los registros del backend. Si el navegador muestra un 500 y los registros del servidor indican:
Database connection failed
Estás lidiando con un problema de backend o infraestructura, no con un error de renderizado. Un frontend que muestra un estado de error claro para las solicitudes fallidas, en lugar de una página vacía, hace que esta distinción sea visible tanto para los usuarios como para los desarrolladores.
10. La autenticación funciona localmente y falla después del despliegue
Los flujos de inicio de sesión son especialmente sensibles a las diferencias entre entornos. Localmente, la secuencia podría ser:
localhost → Login → Dashboard
En producción, se convierte en:
Production → Login → Dashboard doesn't load
Varios problemas anteriores pueden causar esto por sí solos. El frontend aún podría llamar a la URL de API incorrecta, las reglas CORS del backend podrían no incluir el origen de producción, o los tokens podrían no estar configurados correctamente para el nuevo entorno. Sin embargo, las cookies son la causa más frecuente.
Por qué las cookies se comportan de manera diferente en producción
Si un navegador almacena y envía una cookie depende de sus atributos: Secure, HttpOnly, SameSite, Domain y Path. A nivel local, todo suele compartir un mismo host, por lo que esos atributos rara vez son relevantes. En entornos de producción, el frontend y la API pueden estar en dominios diferentes, a través de HTTPS, y entonces el navegador comienza a aplicar las reglas. Por ejemplo, una cookie marcada como SameSite=None solo se acepta cuando también está marcada como Secure.
Las solicitudes entre dominios que incluyen credenciales también requieren la cooperación de ambas partes. El cliente debe autorizar el envío de credenciales, y el servidor debe permitir explícitamente dichas credenciales para ese origen específico; no se aceptan orígenes genéricos para solicitudes con credenciales. Si alguna de las partes no cumple con su parte, la solicitud de inicio de sesión podría parecer exitosa mientras que la cookie de sesión nunca llega en las llamadas posteriores.
Conclusión: un flujo de inicio de sesión que funciona en localhost solo se ha probado en el entorno más sencillo posible. Pruébelo nuevamente con HTTPS similar al de producción, dominios, configuraciones de cookies y reglas CORS.
Método de depuración capa por capa
Cuando falla un despliegue, la respuesta menos efectiva es editar componentes al azar y esperar. La respuesta eficaz es averiguar qué capa está fallando antes de tocar cualquier código.
Comience con la consola
Abra las herramientas de desarrollador del navegador (generalmente F12) y lea la consola. Mensajes como Failed to fetch, violaciones de CORS, excepciones de JavaScript no capturadas y errores 404 para scripts o recursos apuntan cada uno a un problema diferente.
Lea la pestaña Network
Pase a la pestaña Network y vuelva a cargar la página. Para cada solicitud puede ver la URL exacta, el método HTTP, el código de estado, el cuerpo de la respuesta y, a menudo, los encabezados. Para problemas con APIs, esta es la vista más informativa que tiene. Puede ver:
GET /api/users
Status: 404
o:
GET /api/users
Status: 401
o:
GET /api/users
Status: 500
Cada código de estado reduce el rango de búsqueda:
404: el recurso o ruta solicitada no existe en esa URL.401: se requiere autenticación, o las credenciales enviadas faltan o son inválidas.
403: el servidor comprendió quién es usted pero se niega a autorizar la acción.500: el servidor sufrió un error interno al procesar la solicitud.Verifique la URL que realmente utilizó el navegador
No confíe en lo que cree que hace el código fuente; observe la solicitud que realmente envió el navegador. Si la aplicación en producción llama a:
http://localhost:8080/api/users
mientras que el backend se encuentra en realidad en:
https://api.example.com/api/users
casi con certeza ha encontrado el problema, y se trata del problema de la URL de la API mencionado en la sección 1.
Sigua los errores del servidor hasta los registros del backend
Un 500 indica que la respuesta se encuentra en el servidor. Los registros del backend pueden revelar una conexión a la base de datos fallida, una variable faltante o una excepción que el frontend no puede reportar. Esta costumbre de pasar a la siguiente capa cuando las pruebas lo indican es lo que hace que la depuración de despliegues sea rápida.
Análisis de un fallo real
Imaginemos una aplicación full-stack que se despliega sin errores, pero cuyo panel de control muestra contenido vacío. La reacción instintiva es suponer que el componente del panel de control tiene un error:
"Debe haber algo mal con el componente del panel de control."
En lugar de abrir el componente, abra la pestaña Network. Allí encontrará:
GET https://api.example.com/api/dashboard
Status: 500
Eso ya descarta al frontend como causa raíz: la solicitud fue enviada. A continuación, los registros del backend muestran:
Database connection failed
Con ambas pruebas, toda la cadena se vuelve clara:
React Frontend
↓
API request
↓
Backend
↓
Database connection failed
↓
500 response
El componente del panel de control nunca tuvo la culpa. Diez minutos leyendo las herramientas del navegador y los registros del servidor sustituyeron a lo que podrían haber sido horas de refactorización del código funcional. Saber leer los errores del navegador, el tráfico de red y los registros del servidor al mismo tiempo es una de las habilidades más valiosas en el trabajo diario con software.
Una lista de verificación antes del despliegue
La forma más fiable de reducir las sorpresas durante el despliegue es rastrear, antes de enviarlo, cada paso que da una solicitud desde el navegador hasta la base de datos. Una lista de verificación práctica:
- Ejecutar
npm run buildlocalmente y confirmar que tenga éxito, luego verificarlo connpm run preview. - Confirmar que la URL de la API de producción esté configurada y que ninguna solicitud apunte a
localhost.
Ninguna lista de verificación garantiza un lanzamiento impecable, pero esta detecta la mayoría de los problemas comunes mientras aún es económico solucionarlos. Para obtener una visión más amplia de lo que debería incluir una configuración de proyecto sólida desde el primer día, la base de React lista para producción aborda el aspecto relacionado con las herramientas.
Por qué “funciona localmente” demuestra tan poco
Su máquina representa un entorno controlado y tolerante. Usted sabe dónde se ejecuta el backend, qué variables están definidas, qué se instala en node_modules y qué base de datos utiliza la aplicación. En el entorno de producción casi todo cambia: un dominio diferente, una dirección API distinta, variables de entorno distintas, una base de datos diferente, reglas más estrictas para CORS y cookies, un servidor al que hay que indicar cómo manejar las rutas del lado del cliente, y a menudo un sistema operativo distinto. El código puede estar completamente correcto mientras que la configuración que lo rodea es incorrecta.
Por eso, el despliegue merece ser tratado como otro entorno contra el que probar, y no como un paso final que ocurre una vez terminado el desarrollo. Los entornos de pruebas, las compilaciones CI y los despliegues de vista previa existen precisamente para adelantar esa fase de pruebas.
Puntos clave
- Cuando algo se rompe después del despliegue, pregunte qué capa está fallando antes de indagar cuál es el problema con React.
- Si nada se carga, revise la salida de compilación y los recursos; si la página sí se carga pero no los datos, consulte la pestaña Network y la URL de la API.
- Un código
500lo dirige a los registros del backend; un error CORS lo lleva a las orígenes permitidos por el backend. - Una ruta que funciona al hacer clic pero muestra 404 al actualizarla necesita un mecanismo de respaldo en tipo SPA en el servidor, no cambios en el router.
- Los valores faltantes suelen indicar que las variables de entorno de producción nunca se definieron, o que la aplicación no se volvió a compilar después de establecerlas.
- Los fallos de autenticación suelen estar relacionados con las URLs de la API, los problemas CORS y los atributos de cookies, así que verifique los tres.
Encontrarse con estos problemas no es señal de habilidades de programación deficientes; es así como todos aprenden de qué están hechos los sistemas reales. El objetivo no es nunca ver un error, sino ser rápido para localizar dónde se encuentra realmente.