Home / DevOps & Cloud / Cómo desplegar una app React con React Router en Netlify sin 404
Cómo desplegar una app React con React Router en Netlify sin 404

Cómo desplegar una app React con React Router en Netlify sin 404

25 October 2026 · 21 min de lectura ··

Del entorno local a producción: el reto de publicar una SPA con rutas

Construir una aplicación de una sola página con React y React Router es relativamente sencillo mientras trabajas en tu máquina. El servidor de desarrollo de Vite responde a cualquier URL con el mismo documento HTML, React Router lee la ruta del navegador y pinta el componente correcto. Todo funciona. El problema aparece el día en que publicas el proyecto, compartes un enlace profundo como /productos/nuevo con un compañero y él recibe una página de error en lugar de tu formulario.

Netlify es una de las plataformas más cómodas para alojar este tipo de proyectos. Ofrece un plan gratuito generoso, despliegues atómicos que nunca dejan el sitio a medio publicar, historial completo de versiones con reversión inmediata, vistas previas por cada pull request y certificados HTTPS automáticos. Aun así, para desplegar una aplicación React en Netlify que dependa de rutas del lado del cliente necesitas entender una pieza clave: cómo le indicas al servidor que todas las rutas desconocidas deben resolverse con tu index.html.

En esta guía vas a recorrer el proceso completo con herramientas actuales: React 19, React Router v8 (publicado en junio de 2026), Vite 8 y la versión más reciente de Netlify CLI. Verás tres formas de publicar, la causa exacta del famoso 404, dos maneras de solucionarlo, cómo personalizar el nombre del proyecto y una colección de errores frecuentes con su solución.

Requisitos previos y versiones utilizadas

Antes de empezar conviene alinear el entorno, porque React Router v8 subió sus requisitos mínimos respecto a versiones anteriores. Según el anuncio oficial, la nueva línea base es Node 22.22.0 o superior, React 19.2.7 o superior y Vite 7 o superior. Además, el paquete react-router-dom desapareció: ahora todo se importa desde react-router, y lo específico del DOM, como RouterProvider, desde react-router/dom.

Para este tutorial se recomienda:

  • Node.js 24 LTS, o Node.js 26, que entra en soporte de largo plazo en octubre de 2026.
  • npm 10 o superior (incluido con Node).
  • Una cuenta gratuita en Netlify.
  • Una cuenta de GitHub, GitLab, Bitbucket o Azure DevOps si quieres despliegue continuo.
  • Git instalado y configurado.

Comprueba tus versiones desde la terminal:

node --version
npm --version
git --version

Una salida válida se vería así:

v24.11.0
10.9.4
git version 2.51.0

Si tu versión de Node es anterior a la 22.22, actualízala con tu gestor favorito (nvm, fnm, Volta o el instalador oficial) antes de continuar. Instalar React Router v8 sobre una versión de Node más antigua puede producir advertencias de motor incompatible o fallos en la compilación.

Preparar una aplicación de ejemplo con Vite y React Router v8

Para que el problema y la solución sean tangibles, vas a crear una pequeña aplicación de inventario para una cafetería. Tendrá una página de inicio, un listado de productos, un formulario para dar de alta un producto y otro para editarlo. Es suficiente para reproducir el comportamiento de las rutas anidadas y dinámicas.

Crea el proyecto con la plantilla de React de Vite e instala React Router:

npm create vite@latest inventario-cafeteria -- --template react
cd inventario-cafeteria
npm install
npm install react-router@latest

La estructura inicial que te interesa es la siguiente:

inventario-cafeteria/
├── index.html
├── package.json
├── public/
├── src/
│   ├── main.jsx
│   ├── App.jsx
│   └── paginas/
│       ├── Inicio.jsx
│       ├── ListaProductos.jsx
│       ├── NuevoProducto.jsx
│       ├── EditarProducto.jsx
│       └── NoEncontrado.jsx
└── vite.config.js

Define el enrutador en src/main.jsx usando el modo de datos de React Router, que es el enfoque recomendado para SPA porque te da acceso a loaders, acciones y manejo de errores por ruta:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { createBrowserRouter } from "react-router";
import { RouterProvider } from "react-router/dom";

import Layout from "./App.jsx";
import Inicio from "./paginas/Inicio.jsx";
import ListaProductos from "./paginas/ListaProductos.jsx";
import NuevoProducto from "./paginas/NuevoProducto.jsx";
import EditarProducto from "./paginas/EditarProducto.jsx";
import NoEncontrado from "./paginas/NoEncontrado.jsx";

const router = createBrowserRouter([
    {
        path: "/",
        Component: Layout,
        children: [
            { index: true, Component: Inicio },
            { path: "productos", Component: ListaProductos },
            { path: "productos/nuevo", Component: NuevoProducto },
            { path: "productos/:productoId/editar", Component: EditarProducto },
            { path: "*", Component: NoEncontrado },
        ],
    },
]);

createRoot(document.getElementById("root")).render(
    <StrictMode>
        <RouterProvider router={router} />
    </StrictMode>
);

El componente Layout actúa como armazón común con la navegación y un Outlet donde se renderizan las páginas hijas:

import { NavLink, Outlet } from "react-router";

export default function Layout() {
    return (
        <div className="contenedor">
            <nav>
                <NavLink to="/" end>
                    Inicio
                </NavLink>
                <NavLink to="/productos">Productos</NavLink>
                <NavLink to="/productos/nuevo">Nuevo producto</NavLink>
            </nav>
            <main>
                <Outlet />
            </main>
        </div>
    );
}

Y una página de edición que lee el parámetro dinámico de la URL:

import { useParams, useNavigate } from "react-router";

export default function EditarProducto() {
    const { productoId } = useParams();
    const navegar = useNavigate();

    function guardar(evento) {
        evento.preventDefault();
        // Aquí llamarías a tu API para persistir los cambios
        navegar("/productos");
    }

    return (
        <form onSubmit={guardar}>
            <h1>Editando el producto {productoId}</h1>
            <label>
                Nombre
                <input name="nombre" defaultValue="Café de olla" />
            </label>
            <button type="submit">Guardar cambios</button>
        </form>
    );
}

Ejecuta npm run dev, navega entre las páginas, recarga en /productos/42/editar y comprobarás que todo funciona en local. Ahora genera la compilación de producción:

npm run build
vite v8.0.12 building client environment for production...
✓ 41 modules transformed.
dist/index.html                   0.46 kB │ gzip:  0.30 kB
dist/assets/index-Bq3kT9xZ.css    1.12 kB │ gzip:  0.58 kB
dist/assets/index-DkP2wL7a.js   236.84 kB │ gzip: 75.21 kB
✓ built in 612ms

Observa que Vite genera la carpeta dist, no build como hacía Create React App. Este detalle importa más adelante, cuando configures el directorio de publicación.

Por qué aparece el 404: enrutamiento en el cliente frente al servidor

Para entender la solución hay que entender quién responde a cada petición. Cuando haces clic en un NavLink, React Router intercepta el evento, actualiza la barra de direcciones mediante la History API del navegador y renderiza el componente adecuado. No hay ninguna petición HTTP al servidor; todo ocurre dentro de JavaScript.

La situación cambia cuando el usuario escribe directamente la URL, pulsa F5 o abre un enlace compartido. En ese momento el navegador sí hace una petición GET /productos/nuevo al servidor. Netlify es, en esencia, una CDN que sirve archivos estáticos: busca un archivo en dist/productos/nuevo/index.html o dist/productos/nuevo.html, no lo encuentra y devuelve su página genérica de “Page not found”.

Puedes verificarlo con curl después de desplegar:

curl -I https://inventario-cafeteria.netlify.app/productos/nuevo
HTTP/2 404
content-type: text/html; charset=utf-8
server: Netlify

El servidor de desarrollo de Vite no tiene este problema porque incluye un fallback de historial integrado: cualquier ruta que no corresponda a un archivo real recibe el index.html. En producción tienes que reproducir ese comportamiento de forma explícita. La técnica correcta no es una redirección, sino una reescritura: el servidor entrega el contenido de index.html con código 200 mientras la URL del navegador permanece intacta, de modo que React Router puede leerla y pintar la vista correspondiente.

Opción 1: publicar arrastrando la carpeta dist con Netlify Drop

La forma más rápida de poner una aplicación en línea es Netlify Drop. No necesitas repositorio ni terminal: compilas en local y subes la carpeta resultante desde el navegador.

Los pasos son:

  1. Ejecuta npm run build en la raíz del proyecto.
  2. Entra en tu panel de Netlify y busca la zona de despliegue manual (también disponible en la página Netlify Drop).
  3. Arrastra la carpeta dist completa, no su contenido suelto, sobre el área indicada.
  4. Espera unos segundos y Netlify te devolverá una URL pública con un subdominio aleatorio terminado en .netlify.app.

Este método es ideal para prototipos, demostraciones rápidas a un cliente o para verificar que la compilación funciona fuera de tu máquina. Su gran desventaja es que cada cambio exige recompilar y volver a arrastrar la carpeta. Tampoco obtienes vistas previas automáticas ni integración con tu flujo de Git.

Un detalle importante: si arrastras la carpeta antes de añadir la regla de reescritura que verás más adelante, el error 404 aparecerá igualmente. La solución funciona exactamente igual con este método porque el archivo de reglas viaja dentro de dist.

Opción 2: despliegue continuo conectando tu repositorio

El enfoque más productivo para proyectos reales es conectar Netlify con tu repositorio. Así, cada git push a la rama principal dispara una compilación y un despliegue en producción, y cada pull request genera una vista previa con su propia URL. Este despliegue continuo desde un repositorio también te deja un historial navegable de versiones al que puedes volver con un clic si algo sale mal.

Primero sube tu proyecto a GitHub:

git init
git add .
git commit -m "Primera versión del inventario de la cafetería"
git branch -M main
git remote add origin [email protected]:tu-usuario/inventario-cafeteria.git
git push -u origin main

Después, en el panel de Netlify:

  1. Pulsa el botón para añadir un nuevo proyecto (Netlify renombró “sites” a “projects” en su interfaz, así que verás “Add new project”) y elige la opción de importar un proyecto existente.
  2. Selecciona tu proveedor de Git y autoriza la aplicación de Netlify. Si es la primera vez, se abrirá una ventana emergente, así que asegúrate de que tu navegador no la bloquee.
  3. Si tu repositorio no aparece en el listado, usa el enlace para configurar la aplicación de Netlify en GitHub y concédele acceso a ese repositorio concreto o a todos los de tu cuenta.
  4. Elige el repositorio inventario-cafeteria.
  5. Revisa la configuración de compilación. Netlify detecta Vite automáticamente y propone npm run build como comando y dist como directorio de publicación.
  6. Pulsa el botón de desplegar y espera a que termine la compilación.

Si la detección automática no rellena los campos, introdúcelos tú. Una configuración típica sería:

Base directory: (vacío)
Build command: npm run build
Publish directory: dist
Functions directory: netlify/functions

Durante la compilación puedes seguir el log en tiempo real. Un despliegue correcto termina con un resumen similar a este:

$ npm run build
vite v8.0.12 building client environment for production...
✓ built in 1.84s

Deploy site
Starting to deploy site from 'dist'
Finished processing build request in 21.3s
Site is live ✨

A partir de este momento cualquier cambio que empujes a main se publica solo. Si algún despliegue rompe la aplicación, en la sección de despliegues puedes elegir una versión anterior y publicarla de nuevo sin tocar el código.

Opción 3: desplegar desde la terminal con Netlify CLI

La tercera vía, muy útil cuando quieres integrar Netlify en tus propios scripts o pipelines de CI, es Netlify CLI. Instálala de forma global:

npm install -g netlify-cli@latest
netlify --version

Inicia sesión y vincula la carpeta del proyecto con un proyecto de Netlify:

netlify login
netlify init

El comando init detecta el framework, sugiere el comando de compilación y el directorio de publicación, y puede crear un netlify.toml por ti. Desde la versión 21 de la CLI, netlify deploy ejecuta la compilación por defecto, por lo que ya no necesitas encadenar netlify build y netlify deploy:

# Despliegue de borrador con URL única para revisar
netlify deploy

# Despliegue a producción
netlify deploy --prod

# Publicar una carpeta ya compilada sin volver a construir
netlify deploy --prod --no-build --dir=dist

Para entornos de integración continua, Netlify recomienda instalar la CLI como dependencia de desarrollo con un archivo de bloqueo, en lugar de instalarla global en cada ejecución. Así evitas que una versión nueva con cambios incompatibles rompa tus builds sin previo aviso:

npm install --save-dev netlify-cli
{
    "scripts": {
        "dev": "vite",
        "build": "vite build",
        "deploy:preview": "netlify deploy",
        "deploy:prod": "netlify deploy --prod"
    }
}

La CLI también incluye netlify dev, que levanta un servidor local que emula las reglas de redirección, cabeceras y funciones de Netlify. Es la manera más fiable de probar tus reglas de reescritura antes de subirlas.

La solución definitiva al error 404: el archivo _redirects

Llegó el momento de resolver el problema principal. Netlify permite definir reglas de redirección y reescritura en un archivo de texto llamado _redirects, sin extensión, que debe estar en el directorio de publicación. Con Vite, la forma más cómoda de lograrlo es colocar el archivo _redirects en la carpeta public, porque Vite copia todo el contenido de public a dist durante la compilación sin modificarlo.

Crea el archivo public/_redirects con una única línea:

/*    /index.html    200

La regla tiene tres partes. El patrón /* coincide con cualquier ruta. El destino /index.html es el documento que se servirá. El código 200 convierte la regla en una reescritura: Netlify entrega el contenido de index.html sin cambiar la URL ni emitir una redirección.

Tras compilar, comprueba que el archivo llegó a su destino:

npm run build
ls dist
_redirects  assets  index.html  vite.svg

Un punto que suele generar dudas es si esta regla romperá la carga de los archivos JavaScript y CSS. No lo hace, porque Netlify aplica por defecto el principio de “shadowing”: si existe un archivo estático que coincide con la ruta solicitada, como /assets/index-DkP2wL7a.js, lo sirve directamente y la regla no se aplica. Solo las rutas que no corresponden a ningún archivo caen en el fallback.

Haz commit y empuja el cambio:

git add public/_redirects
git commit -m "Añade reescritura SPA para React Router"
git push

Cuando termine el despliegue, repite la prueba con curl:

curl -I https://inventario-cafeteria.netlify.app/productos/nuevo
HTTP/2 200
content-type: text/html; charset=UTF-8
server: Netlify

Ahora puedes recargar cualquier ruta, abrir enlaces profundos en otra pestaña o compartirlos por chat y la aplicación cargará correctamente. Este mismo patrón resuelve el error 404 al recargar la página en prácticamente cualquier SPA: Vue Router, Angular, Svelte con enrutamiento cliente o TanStack Router.

Si además necesitas reglas específicas, como redirigir rutas antiguas o hacer de proxy hacia una API, recuerda que las reglas se evalúan de arriba abajo y gana la primera coincidencia. El comodín siempre debe ir al final:

# Rutas antiguas que cambiaron de nombre
/catalogo/*        /productos/:splat           301

# Proxy hacia la API para evitar problemas de CORS
/api/*             https://api.micafeteria.com/:splat   200

# Fallback de la SPA, siempre la última regla
/*                 /index.html                 200

Alternativa declarativa: configurar todo en netlify.toml

Si prefieres centralizar la configuración en un solo archivo versionado, Netlify lee netlify.toml desde la raíz del repositorio. Allí puedes definir el comando de compilación, el directorio de publicación, la versión de Node, las redirecciones y las cabeceras HTTP. La configuración del archivo tiene prioridad sobre la de la interfaz web cuando ambas entran en conflicto.

[build]
  command = "npm run build"
  publish = "dist"

[build.environment]
  NODE_VERSION = "24"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

[[headers]]
  for = "/assets/*"
  [headers.values]
    Cache-Control = "public, max-age=31536000, immutable"

[[headers]]
  for = "/*"
  [headers.values]
    X-Content-Type-Options = "nosniff"
    Referrer-Policy = "strict-origin-when-cross-origin"

Este archivo aporta varias ventajas frente a configurar desde el panel. La configuración queda revisada en cada pull request, cualquier colaborador entiende cómo se construye el proyecto y puedes reproducir el entorno en otra cuenta de Netlify sin pasos manuales. La sección de cabeceras, por ejemplo, permite cachear de forma agresiva los archivos de assets, que Vite nombra con un hash de contenido y por tanto nunca cambian.

Ten en cuenta el orden de procesamiento: las reglas del archivo _redirects se evalúan antes que las de netlify.toml. Para evitar confusiones, lo más sensato es elegir uno de los dos y mantener ahí todas las reglas.

¿Cuál conviene usar? _redirects es más legible para listas largas de redirecciones simples. netlify.toml es mejor cuando necesitas condiciones avanzadas, como redirecciones por país o idioma, o cuando ya usas el archivo para otros ajustes de compilación.

Personalizar el nombre del proyecto y el dominio

El primer despliegue recibe un nombre aleatorio del estilo glittering-cupcake-4f2a1b.netlify.app. Es funcional pero difícil de recordar, sobre todo cuando acumulas varios proyectos en la misma cuenta.

Para cambiar el nombre del proyecto en Netlify:

  1. Abre el proyecto desde tu panel.
  2. Entra en la configuración del proyecto y busca la sección de detalles generales.
  3. Pulsa el botón para cambiar el nombre, escribe uno nuevo y guarda.

El nuevo subdominio queda activo de inmediato. Una convención práctica es reutilizar el nombre del repositorio, de modo que inventario-cafeteria en GitHub corresponda a inventario-cafeteria.netlify.app. Así localizas cualquier aplicación rápidamente.

Si tienes un dominio propio, añádelo desde la sección de gestión de dominios. Netlify te indicará los registros DNS que debes crear (normalmente un registro CNAME apuntando a tu subdominio de Netlify para un subdominio como app.micafeteria.com) y emitirá un certificado HTTPS gratuito de forma automática una vez que el DNS se propague.

Gestionar páginas 404 propias dentro de React Router

Con la regla de reescritura activa, Netlify ya nunca devolverá su página de error para rutas de la aplicación. Eso significa que la responsabilidad de mostrar un mensaje de “página no encontrada” pasa a tu código. La ruta comodín path: "*" que definiste en el enrutador cumple ese papel:

import { Link, useLocation } from "react-router";

export default function NoEncontrado() {
    const { pathname } = useLocation();

    return (
        <section>
            <h1>No encontramos esta página</h1>
            <p>La ruta {pathname} no existe en el inventario.</p>
            <Link to="/">Volver al inicio</Link>
        </section>
    );
}

Hay un matiz importante para SEO y monitorización: como la reescritura responde siempre con código 200, los buscadores verán un estado correcto incluso en rutas inexistentes. Para aplicaciones internas o paneles de administración esto rara vez importa. Si tu SPA es pública y te preocupa la indexación, considera añadir una etiqueta noindex en la vista de error o evaluar una solución con prerenderizado.

También puedes capturar errores de carga de datos con la propiedad ErrorBoundary en la definición de la ruta, de forma que un fallo en un loader muestre un mensaje amigable en lugar de una pantalla en blanco:

import { isRouteErrorResponse, useRouteError } from "react-router";

function ErrorProductos() {
    const error = useRouteError();
    if (isRouteErrorResponse(error) && error.status === 404) {
        return <p>Ese producto ya no está en el inventario.</p>;
    }
    return <p>Ocurrió un problema al cargar los productos.</p>;
}

// En la definición de rutas:
// { path: "productos/:productoId/editar", Component: EditarProducto, ErrorBoundary: ErrorProductos }

Errores comunes y cómo evitarlos

Aunque la configuración es breve, hay tropiezos que se repiten en muchos proyectos. Conocerlos de antemano te ahorrará despliegues fallidos.

Colocar _redirects en la raíz del repositorio en lugar de public. Netlify busca el archivo en el directorio de publicación. Si lo dejas junto a package.json, Vite no lo copiará a dist y la regla nunca se aplicará.

Guardar el archivo con extensión. Algunos editores en Windows añaden .txt de forma silenciosa. El nombre debe ser exactamente _redirects. Comprueba con ls -la public que no aparece _redirects.txt.

Usar 301 o 302 en lugar de 200. Una redirección cambia la URL del navegador a /index.html, React Router no encuentra esa ruta y terminas en la página de error de tu propia aplicación. El código correcto para una SPA es 200.

Seguir importando desde react-router-dom. En React Router v8 el paquete ya no existe. Las importaciones generales vienen de react-router y RouterProvider de react-router/dom:

// Antes (v6 y v7)
import { Link, useNavigate } from "react-router-dom";

// Ahora (v8)
import { Link, useNavigate } from "react-router";
import { RouterProvider } from "react-router/dom";

Configurar mal el directorio de publicación. Proyectos migrados desde Create React App suelen arrastrar build como carpeta de salida en la configuración de Netlify, mientras Vite genera dist. El resultado es un despliegue vacío o un error indicando que el directorio no existe.

Olvidar la propiedad base de Vite al publicar en un subdirectorio. Si tu aplicación no vive en la raíz del dominio, debes ajustar base en vite.config.js y basename en el enrutador. En Netlify, con un subdominio propio, lo habitual es dejar ambos en su valor por defecto.

Solución de problemas frecuentes

Cuando algo falla, el mensaje de error suele apuntar directamente a la causa. Estos son los más habituales.

El build falla con un error de módulo no encontrado que en local no ocurre:

[vite]: Rollup failed to resolve import "./paginas/listaProductos.jsx" from "src/main.jsx".

Netlify compila sobre Linux, cuyo sistema de archivos distingue mayúsculas y minúsculas. En macOS o Windows ListaProductos.jsx y listaProductos.jsx pueden coincidir, pero no en el servidor. Revisa que el nombre del archivo y la ruta de importación sean idénticos.

El navegador muestra una pantalla en blanco y la consola indica un tipo MIME incorrecto:

Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/html".

Significa que el navegador pidió un archivo JavaScript que no existe y la regla de reescritura le devolvió index.html. Las causas típicas son un valor incorrecto de base en Vite o una caché del navegador apuntando a un hash de un despliegue anterior. Revisa base, vacía la caché y vuelve a desplegar.

La compilación se detiene por la versión de Node:

npm warn EBADENGINE Unsupported engine {
  package: '[email protected]',
  required: { node: '>=22.22.0' },
  current: { node: 'v20.18.0' }
}

Fija la versión de Node con la variable NODE_VERSION en netlify.toml o crea un archivo .nvmrc en la raíz con el contenido 24. Netlify respeta ambos.

Conflictos de dependencias pares durante la instalación:

npm error ERESOLVE unable to resolve dependency tree
npm error Found: [email protected]
npm error Could not resolve dependency:
npm error peer react@">=19.2.7" from [email protected]

React Router v8 exige React 19.2.7 o superior. Actualiza ambos paquetes juntos con npm install react@latest react-dom@latest en lugar de forzar la instalación con --legacy-peer-deps, que solo esconde el problema.

Las variables de entorno llegan como undefined en el navegador. Vite solo expone al cliente las variables con prefijo VITE_. Defínelas en el panel de Netlify o en netlify.toml con ese prefijo y nunca guardes secretos en ellas, porque terminan incrustadas en el bundle público.

Buenas prácticas para producción

Más allá de que la aplicación cargue, hay decisiones que marcan la diferencia en un proyecto que vas a mantener durante años.

Versiona toda la configuración. Mantén netlify.toml en el repositorio con el comando de compilación, el directorio de publicación, la versión de Node y las reglas. Así cualquier cambio queda auditado en tu historial de Git.

Aprovecha las vistas previas de despliegue. Cada pull request obtiene su propia URL, lo que permite que diseño, producto o QA revisen los cambios antes de fusionarlos. Puedes añadir protección por contraseña a estas vistas si el proyecto es privado.

Aplica cabeceras de seguridad. Además de X-Content-Type-Options y Referrer-Policy, valora una Content-Security-Policy adaptada a tus dominios de API y analítica:

[[headers]]
  for = "/*"
  [headers.values]
    Content-Security-Policy = "default-src 'self'; connect-src 'self' https://api.micafeteria.com; img-src 'self' data:"
    X-Frame-Options = "DENY"

Divide el código por rutas. Con lazy en la definición de rutas, React Router descarga el código de cada página solo cuando el usuario la visita, reduciendo el tamaño del bundle inicial:

const router = createBrowserRouter([
    {
        path: "/",
        Component: Layout,
        children: [
            { index: true, Component: Inicio },
            {
                path: "productos/nuevo",
                lazy: async () => {
                    const modulo = await import("./paginas/NuevoProducto.jsx");
                    return { Component: modulo.default };
                },
            },
        ],
    },
]);

Automatiza las comprobaciones. Ejecuta pruebas y linting antes de compilar para que un error no llegue a producción. Si el comando de build falla, Netlify mantiene la versión anterior publicada:

{
    "scripts": {
        "build": "npm run lint && npm run test -- --run && vite build"
    }
}

Mantén las dependencias al día con Dependabot o Renovate. React Router ha adoptado un ciclo de una versión mayor al año, y sus cambios incompatibles se pueden adoptar con antelación mediante flags futuros, lo que facilita migraciones graduales.

Netlify frente a otras plataformas de despliegue

Netlify no es la única opción para alojar una SPA con React Router. Cada alternativa tiene su propio mecanismo para resolver la reescritura de rutas.

Vercel ofrece una experiencia muy parecida, con despliegue desde Git y vistas previas. Para una SPA pura defines la reescritura en vercel.json:

{
    "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}

Cloudflare Pages destaca por su red global y por un plan gratuito sin límite de ancho de banda en sitios estáticos. Con proyectos sin archivo 404.html, aplica el comportamiento de SPA automáticamente, y también admite un archivo _redirects con sintaxis casi idéntica a la de Netlify.

GitHub Pages es gratuito y sencillo, pero no admite reglas de reescritura. La solución habitual es copiar index.html como 404.html, lo que funciona visualmente pero responde con código 404 real, algo poco deseable para SEO y monitorización.

Un servidor propio con Nginx te da control total a cambio de mantener la infraestructura:

location / {
  try_files $uri $uri/ /index.html;
}

Elige Netlify cuando valores la combinación de despliegue continuo, formularios, funciones serverless, vistas previas y configuración declarativa sin administrar servidores. Considera Cloudflare Pages si tu tráfico es muy alto y quieres minimizar costes, y un servidor propio si necesitas reglas de red muy específicas o cumplir requisitos de alojamiento concretos.

Casos de uso reales

El patrón descrito en esta guía encaja en escenarios muy variados. Los paneles internos de administración, como un backoffice de inventario o un CRM ligero, son el caso más común: dependen totalmente de rutas del cliente y no necesitan renderizado en servidor.

Los portafolios y sitios de producto con varias secciones también se benefician de la reescritura, especialmente si comparten enlaces directos a proyectos concretos en redes sociales.

Las aplicaciones que consumen una API externa, como un gestor de reservas que habla con un backend en Node o Python, pueden combinar el fallback de la SPA con una regla de proxy para /api/*. Así el frontend y el backend parecen vivir bajo el mismo dominio y evitas configurar CORS.

Por último, si con el tiempo tu proyecto necesita renderizado en servidor, React Router en modo framework se despliega también en Netlify mediante su plugin oficial para Vite. En ese escenario ya no necesitas la reescritura manual, porque las funciones de servidor resuelven cada ruta. Para la mayoría de SPA, sin embargo, una sola línea en _redirects sigue siendo la solución más simple y eficaz.

Conclusiones

Publicar una aplicación React con React Router en Netlify requiere pocos pasos, pero hay que entender por qué cada uno es necesario. La navegación interna funciona gracias a la History API, mientras que las cargas directas y las recargas llegan al servidor, que no conoce tus rutas. Una regla de reescritura con código 200 cierra esa brecha y permite que React Router tome el control desde index.html.

Has visto tres formas de desplegar: arrastrar la carpeta dist para pruebas rápidas, conectar el repositorio para obtener despliegue continuo con vistas previas y usar Netlify CLI para integrar la publicación en scripts y pipelines. También aprendiste a definir la regla en _redirects o en netlify.toml, a personalizar el nombre del proyecto y el dominio, y a gestionar tus propias páginas de error desde el enrutador.

Con React Router v8, recuerda las nuevas bases: Node 22.22 o superior, React 19.2.7 o superior, Vite 7 o superior e importaciones desde react-router y react-router/dom. Si mantienes la configuración versionada, fijas la versión de Node y aplicas cabeceras de caché y seguridad, tendrás una SPA rápida, confiable y lista para crecer.