Home / Frontend / Vue Router en Vue 3: guía completa de rutas, guardias y transiciones
Vue Router en Vue 3: guía completa de rutas, guardias y transiciones

Vue Router en Vue 3: guía completa de rutas, guardias y transiciones

30 October 2026 · 19 min de lectura ··

Una aplicación de página única carga un solo documento HTML y, a partir de ahí, JavaScript se encarga de mostrar las distintas pantallas. El visitante tiene la sensación de moverse entre páginas, pero el navegador nunca descarga un documento nuevo: solo cambia la URL y se reemplaza el componente visible. El resultado es una navegación instantánea, sin parpadeos y con la posibilidad de conservar el estado entre vistas.

Para que esa experiencia funcione bien hace falta algo más que mostrar y ocultar componentes. La URL debe reflejar la pantalla actual, los botones de atrás y adelante del navegador deben comportarse como el usuario espera, los enlaces deben poder compartirse y abrirse en otra pestaña, y ciertas secciones deben protegerse si el usuario no inició sesión. Esa es la tarea de Vue Router, el enrutador oficial del ecosistema Vue.

En esta guía construirás desde cero la tienda en línea de un tostador de café ficticio llamado Tostadero, con página de inicio, catálogo, ficha de producto, área de cuenta protegida y página de error. Por el camino verás cómo configurar Vue Router en Vue 3 con la Composition API, cómo trabajar con parámetros dinámicos, rutas anidadas, guardias, carga diferida, transiciones y el nuevo enrutamiento basado en archivos.

Qué versiones usar hoy

Muchos tutoriales antiguos siguen mostrando Vue 2, Vue Router 3 y la herramienta Vue CLI con comandos como vue init webpack. Ese ecosistema quedó atrás: Vue 2 llegó al final de su soporte oficial y Vue CLI está en modo de mantenimiento. El punto de partida actual es este:

  • Vue 3, con la rama estable 3.5.
  • Vue Router 5, publicado en 2026 como una versión de transición que integra en el núcleo el enrutamiento basado en archivos y mantiene compatibles las API de la versión 4.
  • create-vue, el generador oficial de proyectos basado en Vite.

La buena noticia de Vue Router 5 es que, para quien usaba la versión 4 sin enrutamiento por archivos, no hay cambios incompatibles: createRouter, createWebHistory, RouterLink, RouterView, useRoute y useRouter funcionan igual. Lo nuevo es opcional y lo veremos al final.

Crear el proyecto con create-vue

El generador oficial crea un proyecto con Vite y te pregunta qué piezas quieres incluir. Basta con responder que sí a la opción del enrutador:

npm create vue@latest tostadero
cd tostadero
npm install
npm run dev
  VITE ready in 412 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose

Si ya tienes un proyecto Vue sin enrutador, puedes añadirlo manualmente:

npm install vue-router@5

Organizaremos el código separando las vistas, que corresponden a rutas, de los componentes reutilizables:

tostadero/
├── index.html
├── package.json
├── vite.config.js
└── src/
    ├── main.js
    ├── App.vue
    ├── router/
    │   └── index.js
    ├── data/
    │   └── cafes.js
    ├── components/
    │   └── TarjetaCafe.vue
    └── views/
        ├── InicioView.vue
        ├── CatalogoView.vue
        ├── CafeDetalleView.vue
        ├── CuentaView.vue
        ├── CuentaPedidosView.vue
        ├── CuentaPerfilView.vue
        ├── LoginView.vue
        └── NoEncontradoView.vue

Definir las primeras rutas

El corazón del enrutador es un arreglo de rutas. Cada una asocia una ruta de URL con el componente que debe mostrarse. La función createRouter recibe ese arreglo y un modo de historial:

// src/router/index.js
import { createRouter, createWebHistory } from "vue-router";
import InicioView from "../views/InicioView.vue";

const routes = [
    {
        path: "/",
        name: "inicio",
        component: InicioView,
    },
    {
        path: "/catalogo",
        name: "catalogo",
        component: () => import("../views/CatalogoView.vue"),
    },
];

const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes,
});

export default router;

Cada ruta tiene tres propiedades clave:

  • path: el patrón de la URL.
  • name: un identificador para navegar sin escribir rutas a mano.
  • component: el componente que se renderiza.

La segunda ruta usa una función con import() en lugar de una importación estática. Esa diferencia, aparentemente menor, divide el código en archivos separados y lo veremos en detalle más adelante.

Después registras el enrutador en la aplicación:

// src/main.js
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";

createApp(App).use(router).mount("#app");

En Vue 2 esto se hacía con Vue.use(Router) y new Router({...}). En Vue 3 todo se basa en funciones fábrica y en app.use(), lo que permite crear varias aplicaciones independientes en una misma página.

Con las rutas definidas, faltan dos piezas en la interfaz: los enlaces de navegación y el lugar donde se dibuja la vista activa. Vue Router aporta dos componentes globales para ello.

<!-- src/App.vue -->
<script setup>
import { RouterLink, RouterView } from "vue-router";
</script>

<template>
    <header class="barra">
        <RouterLink to="/" class="marca">Tostadero</RouterLink>
        <nav>
            <RouterLink to="/">Inicio</RouterLink>
            <RouterLink :to="{ name: 'catalogo' }">Catálogo</RouterLink>
            <RouterLink :to="{ name: 'cuenta' }">Mi cuenta</RouterLink>
        </nav>
    </header>

    <main>
        <RouterView />
    </main>
</template>

RouterLink genera una etiqueta <a> real, por lo que es accesible, se puede abrir en otra pestaña y funciona con clic central. Sin embargo, intercepta el clic normal para cambiar de vista sin recargar. Usar una etiqueta <a href> común provocaría una recarga completa y perderías el estado de la aplicación.

RouterView es el hueco donde aparece el componente de la ruta activa. Todo lo que esté fuera de él, como la cabecera, permanece montado mientras navegas.

RouterLink añade automáticamente dos clases al enlace activo: router-link-active cuando la ruta actual empieza por la del enlace, y router-link-exact-active cuando coincide exactamente. Puedes estilizarlas para resaltar la sección actual:

nav a.router-link-exact-active {
    color: #6f3d1f;
    border-block-end: 2px solid currentColor;
}

Rutas dinámicas con parámetros

Un catálogo con cientos de cafés no puede tener una ruta por producto. Las rutas dinámicas en Vue Router resuelven esto con segmentos variables precedidos por dos puntos. Primero, unos datos de ejemplo que simulan una API:

// src/data/cafes.js
export const cafes = [
    {
        slug: "etiopia-yirgacheffe",
        nombre: "Etiopía Yirgacheffe",
        tueste: "claro",
        precio: 14.5,
    },
    {
        slug: "colombia-huila",
        nombre: "Colombia Huila",
        tueste: "medio",
        precio: 12.9,
    },
    {
        slug: "guatemala-antigua",
        nombre: "Guatemala Antigua",
        tueste: "medio-oscuro",
        precio: 13.4,
    },
];

export function buscarCafe(slug) {
    return cafes.find((cafe) => cafe.slug === slug);
}

Ahora la ruta con parámetro:

{
  path: '/cafe/:slug',
  name: 'cafe-detalle',
  component: () => import('../views/CafeDetalleView.vue'),
}

Desde el catálogo, el enlace se construye con el nombre de la ruta y sus parámetros. Navegar por nombre es más robusto que concatenar cadenas: si un día cambias la URL a /cafes/:slug, los enlaces siguen funcionando.

<!-- src/views/CatalogoView.vue -->
<script setup>
import { cafes } from "../data/cafes";
</script>

<template>
    <h1>Nuestros cafés</h1>
    <ul class="rejilla">
        <li v-for="cafe in cafes" :key="cafe.slug">
            <RouterLink
                :to="{ name: 'cafe-detalle', params: { slug: cafe.slug } }"
            >
                {{ cafe.nombre }} · {{ cafe.precio }} €
            </RouterLink>
        </li>
    </ul>
</template>

En la vista de detalle, el composable useRoute da acceso a la ruta actual y a sus parámetros:

<!-- src/views/CafeDetalleView.vue -->
<script setup>
import { computed } from "vue";
import { useRoute } from "vue-router";
import { buscarCafe } from "../data/cafes";

const route = useRoute();
const cafe = computed(() => buscarCafe(route.params.slug));
</script>

<template>
    <article v-if="cafe">
        <h1>{{ cafe.nombre }}</h1>
        <p>Tueste {{ cafe.tueste }} · {{ cafe.precio }} €</p>
    </article>
    <p v-else>No encontramos ese café.</p>
</template>

El uso de computed es importante. Cuando navegas de un café a otro, Vue reutiliza la misma instancia del componente porque la ruta es idéntica y solo cambia el parámetro. Si leyeras el parámetro una sola vez al montar, la vista mostraría el producto anterior. Una propiedad computada se recalcula automáticamente cuando cambia route.params.

En la API de opciones de Vue 2 se usaba this.$route.params. Sigue disponible en componentes con Options API, pero en <script setup> la forma recomendada son los composables useRoute y useRouter.

Recibir parámetros como props

Leer route.params dentro del componente lo acopla al enrutador y complica las pruebas. Con la opción props: true, los parámetros llegan como props normales:

{
  path: '/cafe/:slug',
  name: 'cafe-detalle',
  component: () => import('../views/CafeDetalleView.vue'),
  props: true,
}
<script setup>
import { computed } from "vue";
import { buscarCafe } from "../data/cafes";

const props = defineProps({
    slug: { type: String, required: true },
});

const cafe = computed(() => buscarCafe(props.slug));
</script>

Ahora el componente puede reutilizarse fuera del enrutador o probarse pasando el prop directamente. También puedes usar una función para transformar valores, por ejemplo convertir un identificador numérico:

{
  path: '/pedido/:id(\\d+)',
  component: () => import('../views/PedidoView.vue'),
  props: (route) => ({ id: Number(route.params.id) }),
}

La expresión (\\d+) restringe el parámetro a dígitos, de modo que /pedido/abc no coincidirá con esta ruta.

No toda la navegación nace de un enlace. Tras enviar un formulario o completar una compra, necesitas cambiar de vista desde código. El composable useRouter devuelve la instancia del enrutador con métodos como push, replace, back y go:

<script setup>
import { ref } from "vue";
import { useRouter } from "vue-router";

const router = useRouter();
const termino = ref("");

function buscar() {
    router.push({
        name: "catalogo",
        query: { q: termino.value, tueste: "medio" },
    });
}
</script>

<template>
    <form @submit.prevent="buscar">
        <input v-model="termino" type="search" placeholder="Buscar café" />
        <button>Buscar</button>
    </form>
</template>

La navegación anterior produce una URL como /catalogo?q=huila&tueste=medio. Los parámetros de consulta son ideales para filtros, ordenación y paginación, porque la URL resultante se puede compartir y conserva el estado de la búsqueda. Se leen con route.query:

const route = useRoute();
const filtrados = computed(() =>
    cafes.filter((cafe) =>
        cafe.nombre.toLowerCase().includes((route.query.q ?? "").toLowerCase())
    )
);

La diferencia entre push y replace es sutil pero relevante: push añade una entrada al historial, mientras que replace sustituye la actual. Usa replace en redirecciones tras iniciar sesión, para que el botón de atrás no devuelva al formulario.

router.push devuelve una promesa que se resuelve cuando la navegación termina o falla. Si una guardia la cancela, la promesa se resuelve con un objeto de fallo que puedes inspeccionar.

Rutas anidadas para secciones con subnavegación

El área de cuenta de Tostadero tiene una estructura común: una barra lateral fija y un panel que cambia entre pedidos y perfil. Las rutas anidadas modelan exactamente eso, con un componente padre que contiene su propio RouterView:

{
  path: '/cuenta',
  component: () => import('../views/CuentaView.vue'),
  meta: { requiereSesion: true },
  children: [
    { path: '', name: 'cuenta', redirect: { name: 'cuenta-pedidos' } },
    { path: 'pedidos', name: 'cuenta-pedidos', component: () => import('../views/CuentaPedidosView.vue') },
    { path: 'perfil', name: 'cuenta-perfil', component: () => import('../views/CuentaPerfilView.vue') },
  ],
}
<!-- src/views/CuentaView.vue -->
<template>
    <section class="cuenta">
        <aside>
            <RouterLink :to="{ name: 'cuenta-pedidos' }"
                >Mis pedidos</RouterLink
            >
            <RouterLink :to="{ name: 'cuenta-perfil' }">Perfil</RouterLink>
        </aside>
        <div class="panel">
            <RouterView />
        </div>
    </section>
</template>

Observa que las rutas hijas no empiezan con barra: pedidos se resuelve como /cuenta/pedidos. Si escribieras /pedidos, la ruta se trataría como absoluta. La ruta hija vacía con redirect envía a los pedidos cuando alguien visita /cuenta directamente.

Redirecciones, alias y página 404

Las redirecciones mantienen vivas URL antiguas tras una reorganización. Los alias permiten que un mismo contenido responda a varias URL sin redirigir. Y toda aplicación necesita una ruta comodín para las direcciones que no existen:

const routes = [
    // ...rutas anteriores
    { path: "/tienda", redirect: { name: "catalogo" } },
    {
        path: "/catalogo",
        name: "catalogo",
        alias: "/cafes",
        component: () => import("../views/CatalogoView.vue"),
    },
    {
        path: "/:rutaPerdida(.*)*",
        name: "no-encontrado",
        component: () => import("../views/NoEncontradoView.vue"),
    },
];

La sintaxis /:rutaPerdida(.*)* captura cualquier ruta no reconocida. En Vue Router 3 se usaba el asterisco simple *, que dejó de funcionar a partir de la versión 4. El orden de las rutas no afecta a la coincidencia, porque el enrutador ordena por especificidad, así que la ruta comodín no bloqueará a las demás.

Un detalle importante: el enrutador muestra la vista de error en el cliente, pero el servidor sigue respondiendo con código 200. Si te importa el SEO, valida en el servidor o usa renderizado en el servidor para devolver un 404 real.

Guardias de navegación para proteger secciones

Las guardias de navegación en Vue son funciones que se ejecutan antes, durante o después de cada cambio de ruta. Permiten cancelar navegaciones, redirigir o cargar datos. El caso más típico es proteger rutas privadas usando el campo meta:

// src/router/index.js
import { useSesion } from "../stores/sesion";

router.beforeEach((to) => {
    const sesion = useSesion();

    if (to.meta.requiereSesion && !sesion.autenticado) {
        return {
            name: "login",
            query: { volver: to.fullPath },
        };
    }
});

La guardia devuelve un objeto de ruta para redirigir, false para cancelar, o nada para permitir la navegación. El antiguo tercer parámetro next todavía se acepta, pero devolver valores es la forma recomendada porque evita errores como llamar a next() dos veces o ninguna.

El campo meta se hereda de padres a hijos: al marcar /cuenta con requiereSesion, las rutas /cuenta/pedidos y /cuenta/perfil quedan protegidas automáticamente.

Tras iniciar sesión, la vista de login devuelve al usuario al lugar que intentaba visitar:

async function entrar() {
    await sesion.iniciar(correo.value, clave.value);
    const destino = route.query.volver;
    router.replace(
        typeof destino === "string" && destino.startsWith("/") ? destino : "/"
    );
}

La comprobación startsWith('/') evita una vulnerabilidad de redirección abierta, en la que un atacante podría construir un enlace que lleve a otro dominio tras el login.

Existen guardias en tres niveles:

  • Globales: beforeEach, beforeResolve y afterEach, sobre el enrutador.
  • Por ruta: beforeEnter, dentro de la definición de una ruta.
  • Dentro del componente: onBeforeRouteLeave y onBeforeRouteUpdate.

Un uso práctico de la guardia de componente es evitar que el usuario pierda cambios sin guardar:

<script setup>
import { ref } from "vue";
import { onBeforeRouteLeave } from "vue-router";

const hayCambios = ref(false);

onBeforeRouteLeave(() => {
    if (hayCambios.value) {
        return window.confirm(
            "Tienes cambios sin guardar. ¿Salir de todos modos?"
        );
    }
});
</script>

Y afterEach es un buen lugar para actualizar el título del documento:

router.afterEach((to) => {
    document.title = to.meta.titulo
        ? `${to.meta.titulo} · Tostadero`
        : "Tostadero";
});

Recuerda que las guardias del cliente mejoran la experiencia, pero no son seguridad real: cualquiera puede modificar el JavaScript en su navegador. La autorización definitiva siempre debe verificarse en la API del servidor.

Carga diferida de rutas

En una aplicación grande, empaquetar todas las vistas en un único archivo hace que la primera carga sea lenta. La carga diferida de rutas en Vue divide el código por vistas, de modo que cada una se descarga solo cuando se visita. Ya la has usado sin darte cuenta:

// Carga inmediata: forma parte del paquete principal
import InicioView from "../views/InicioView.vue";

// Carga diferida: Vite genera un archivo separado
const CafeDetalleView = () => import("../views/CafeDetalleView.vue");

Tras compilar con npm run build, verás varios archivos de JavaScript en lugar de uno:

dist/assets/index-B2kq9d1x.js              74.12 kB │ gzip: 29.30 kB
dist/assets/CatalogoView-Dx81hQ2a.js         2.41 kB │ gzip:  1.12 kB
dist/assets/CafeDetalleView-Cq7mWp3e.js      1.87 kB │ gzip:  0.94 kB
dist/assets/CuentaView-a9Lk2Zx0.js           1.32 kB │ gzip:  0.71 kB

Como regla práctica, carga de forma inmediata la página de inicio, que casi todos visitan, y difiere el resto. Para secciones grandes con muchas vistas, puedes agrupar varias en un mismo fragmento desde la configuración de Vite.

Transiciones entre vistas

Animar el cambio de vista suaviza la navegación y ayuda al usuario a entender que el contenido cambió. En Vue 3, RouterView expone el componente activo mediante una ranura, que se envuelve con <Transition>:

<template>
    <RouterView v-slot="{ Component, route }">
        <Transition :name="route.meta.transicion || 'desvanecer'" mode="out-in">
            <component :is="Component" :key="route.path" />
        </Transition>
    </RouterView>
</template>

<style>
.desvanecer-enter-active,
.desvanecer-leave-active {
    transition:
        opacity 180ms ease,
        transform 180ms ease;
}

.desvanecer-enter-from {
    opacity: 0;
    transform: translateY(8px);
}

.desvanecer-leave-to {
    opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
    .desvanecer-enter-active,
    .desvanecer-leave-active {
        transition: none;
    }
}
</style>

En Vue 2 era habitual envolver directamente <router-view> con <transition>. En Vue 3 esa forma ya no funciona correctamente y debe usarse la ranura con v-slot. El modo out-in espera a que la vista saliente termine antes de mostrar la nueva, evitando que ambas se superpongan. La clave :key="route.path" fuerza la animación incluso cuando solo cambia un parámetro.

Usar route.meta.transicion permite asignar animaciones distintas por ruta, por ejemplo un deslizamiento lateral para el detalle de producto. Mantén las transiciones cortas: más de 300 milisegundos hacen que la aplicación se sienta lenta.

Comportamiento del desplazamiento

Por defecto, al navegar en una aplicación de página única la posición de desplazamiento no cambia, lo que resulta extraño cuando pasas de una lista larga a un detalle. La opción scrollBehavior lo corrige:

const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes,
    scrollBehavior(to, from, posicionGuardada) {
        if (posicionGuardada) return posicionGuardada;
        if (to.hash) return { el: to.hash, behavior: "smooth" };
        return { top: 0 };
    },
});

Con esta configuración, el botón de atrás restaura la posición anterior, los enlaces con ancla desplazan hasta el elemento y cualquier otra navegación empieza arriba.

Modos de historial y configuración del servidor

Vue Router ofrece tres modos de historial:

  • createWebHistory: URL limpias como /cafe/colombia-huila. Es el recomendado.
  • createWebHashHistory: URL con almohadilla como /#/cafe/colombia-huila. No requiere configurar el servidor, pero las URL son menos limpias.
  • createMemoryHistory: no toca la URL; se usa en renderizado en servidor y en pruebas.

El modo de historial web tiene una consecuencia: si el usuario recarga en /cafe/colombia-huila, el navegador pide esa ruta al servidor, y si no existe un archivo con ese nombre, obtendrás un error 404. Hay que configurar el servidor para devolver index.html ante cualquier ruta desconocida. En Nginx:

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

Las plataformas de alojamiento estático suelen ofrecer una opción equivalente de reescritura para aplicaciones de página única.

Enrutamiento basado en archivos con Vue Router 5

La novedad principal de Vue Router 5 es que el enrutamiento basado en archivos ya forma parte del paquete oficial. Antes requería el plugin externo unplugin-vue-router; ahora se importa directamente desde vue-router/vite:

// vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import VueRouter from "vue-router/vite";

export default defineConfig({
    plugins: [
        VueRouter({ dts: "src/route-map.d.ts" }), // debe ir antes del plugin de Vue
        vue(),
    ],
});

Con este enfoque no escribes el arreglo de rutas: la estructura de carpetas lo genera. Los corchetes indican parámetros:

src/pages/
├── index.vue                 → /
├── catalogo.vue              → /catalogo
├── cafe/
│   └── [slug].vue            → /cafe/:slug
├── cuenta.vue                → layout de /cuenta
├── cuenta/
│   ├── pedidos.vue           → /cuenta/pedidos
│   └── perfil.vue            → /cuenta/perfil
└── [...rutaPerdida].vue      → página 404
// src/router/index.js
import { createRouter, createWebHistory } from "vue-router";
import { routes } from "vue-router/auto-routes";

export const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes,
});

La gran ventaja es el tipado: el archivo route-map.d.ts permite que TypeScript detecte nombres de ruta o parámetros inexistentes. Si migras desde unplugin-vue-router, el cambio consiste sobre todo en ajustar las rutas de importación. Si usas Vue Router 4 con rutas manuales, puedes actualizar a la versión 5 sin modificar tu código y adoptar las rutas por archivos cuando te convenga.

Vue Router frente a otras alternativas

Vue Router es la opción por defecto para aplicaciones Vue, pero no la única. Nuxt, el metaframework de Vue, usa Vue Router internamente y añade renderizado en servidor, enrutamiento por archivos y carga de datos integrada; es preferible cuando el SEO o el tiempo de primera carga son críticos. TanStack Router ofrece una variante para Vue con fuerte énfasis en tipado y gestión de parámetros de búsqueda, interesante para equipos que ya usan el ecosistema TanStack.

Para aplicaciones de panel, herramientas internas y productos detrás de un login, Vue Router con Vite es la combinación más directa. Para sitios públicos de contenido, Nuxt suele compensar su complejidad adicional.

Errores comunes al trabajar con rutas

Estos son los tropiezos más habituales, sobre todo al venir de Vue 2:

  • Usar <a href> en lugar de RouterLink, provocando recargas completas.
  • Leer parámetros una sola vez al montar y no reaccionar cuando cambian.
  • Mezclar path y params en router.push; los parámetros se ignoran si se usa path.
  • Escribir rutas hijas con barra inicial, convirtiéndolas en absolutas.
  • Conservar la ruta comodín * de Vue Router 3.
  • Envolver RouterView directamente con Transition, sin la ranura v-slot.
  • Confiar en guardias del cliente como única capa de seguridad.
  • Olvidar configurar el servidor para el modo de historial web.

El tercer error merece un ejemplo, porque no lanza excepción:

// Incorrecto: params se ignora cuando se usa path
router.push({ path: "/cafe", params: { slug: "colombia-huila" } });

// Correcto: con name
router.push({ name: "cafe-detalle", params: { slug: "colombia-huila" } });

// Correcto: con path completo
router.push(`/cafe/${encodeURIComponent("colombia-huila")}`);

Solución de problemas frecuentes

La consola del navegador suele indicar con claridad qué falla. Estos son los mensajes más comunes y su causa.

Navegar a una ruta con nombre inexistente:

Uncaught Error: No match for
 {"name":"cafe-detalles","params":{"slug":"colombia-huila"}}

El nombre está mal escrito. Revisa el arreglo de rutas o usa el tipado generado para detectarlo en el editor.

Falta un parámetro obligatorio:

Uncaught Error: Missing required param "slug"

Estás navegando a una ruta dinámica sin pasar params, o el valor es undefined.

Aviso de ruta sin coincidencia:

[Vue Router warn]: No match found for location with path "/cafes/huila"

No hay ruta para esa URL. Añade la ruta comodín para mostrar una página de error controlada.

Navegación duplicada al pulsar dos veces el mismo enlace: router.push resuelve la promesa con un fallo de tipo duplicado que puedes ignorar con seguridad. Para distinguir cada tipo de fallo, usa isNavigationFailure:

import { isNavigationFailure, NavigationFailureType } from "vue-router";

const fallo = await router.push({ name: "catalogo" });
if (isNavigationFailure(fallo, NavigationFailureType.aborted)) {
    console.info("Una guardia canceló la navegación");
}

Página en blanco al recargar en producción: el servidor no devuelve index.html para rutas profundas; revisa la configuración descrita en la sección de modos de historial. Si la aplicación se publica en una subcarpeta, configura también la opción base en Vite para que coincida con import.meta.env.BASE_URL.

Buenas prácticas para producción

Antes de publicar una aplicación con enrutador, repasa estos puntos:

  • Navega siempre por nombre de ruta, no por cadenas de URL concatenadas.
  • Usa props: true para desacoplar vistas del enrutador.
  • Difiere la carga de todas las vistas salvo la de inicio.
  • Centraliza permisos y títulos en el campo meta.
  • Valida destinos de redirección para evitar redirecciones abiertas.
  • Gestiona el foco tras cada navegación para usuarios de lectores de pantalla.
  • Configura scrollBehavior y una ruta comodín de error.
  • Si usas TypeScript, considera el enrutamiento por archivos para obtener rutas tipadas.

La accesibilidad suele olvidarse: al cambiar de vista, un lector de pantalla no anuncia nada porque no hubo carga de página. Mover el foco al encabezado principal soluciona el problema:

import { nextTick } from "vue";

router.afterEach(async () => {
    await nextTick();
    document.querySelector("main h1")?.focus();
});

Para que el encabezado reciba el foco, añádele tabindex="-1" en cada vista. Este pequeño ajuste hace que la navegación sea comprensible para quienes no ven la pantalla.

Conclusiones

Vue Router convierte un conjunto de componentes en una aplicación navegable de verdad: URL compartibles, historial funcional, secciones protegidas y transiciones fluidas, todo sin recargar la página. Con Vue 3 y la Composition API, su uso se volvió más explícito y fácil de probar gracias a createRouter, useRoute y useRouter.

Los conceptos clave son pocos y se combinan entre sí: rutas con nombre y parámetros dinámicos, props para desacoplar componentes, rutas anidadas para secciones con subnavegación, guardias para controlar el acceso, carga diferida para mantener ligera la primera visita y transiciones para dar continuidad visual. Dominarlos cubre la gran mayoría de necesidades de una aplicación real.

Vue Router 5 da un paso más al integrar el enrutamiento basado en archivos y el tipado de rutas en el paquete oficial, sin romper el código existente de la versión 4. Si partes de un proyecto antiguo con Vue 2, la migración exige reescribir la configuración, las transiciones y la ruta comodín, pero el resultado es una base moderna, mejor tipada y preparada para crecer.