Routing básico
Hasta ahora has construido todo en una sola página. Pero las aplicaciones reales tienen múltiples vistas — una página de inicio, una de detalles, una de "acerca de". Vue Router te permite crear navegación entre diferentes "páginas" sin recargar el navegador. Esa es la magia de las SPA (Single Page Applications).
Instalar Vue Router
Tienes dos opciones para añadir Vue Router a tu proyecto:
Opción 1 — instalarlo en un proyecto existente:
npm install vue-router
Opción 2 — al crear un proyecto nuevo con npm create vue@latest, selecciona "Yes" cuando te pregunte si quieres añadir Vue Router. Esto configura todo automáticamente.
Configurar el router
La configuración del router vive en su propio archivo. Aquí defines las rutas de tu aplicación — cada ruta conecta una URL con un componente.
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import AboutView from '../views/AboutView.vue'
import CharacterListView from '../views/CharacterListView.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
component: AboutView
},
{
path: '/characters',
name: 'characters',
component: CharacterListView
}
]
})
export default router
Cada ruta tiene tres propiedades clave:
path— la URL que activa esta ruta (/,/about, etc.).name— un identificador único para la ruta. Lo usarás para navegar sin escribir URLs a mano.component— el componente Vue que se renderiza cuando el usuario visita esa URL.
Ahora registra el router en tu archivo principal:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
Y finalmente, coloca <RouterView /> en tu App.vue — es el "hueco" donde Vue Router renderizará el componente de la ruta activa:
<script setup>
import { RouterView } from 'vue-router'
</script>
<template>
<div id="app">
<h1>Mi App Vue</h1>
<RouterView />
</div>
</template>
Crear las vistas (page components)
En Vue existe una convención importante: los componentes que representan páginas completas van en la carpeta src/views/, mientras que los componentes reutilizables (botones, tarjetas, inputs) van en src/components/. No es una regla técnica — Vue los trata igual — pero mantiene tu proyecto organizado.
<script setup>
import { RouterLink } from 'vue-router'
</script>
<template>
<div>
<h2>Bienvenido al Explorador Rick and Morty</h2>
<p>Descubre todos los personajes del multiverso.</p>
<RouterLink to="/characters">Ver personajes →</RouterLink>
</div>
</template>
<template>
<div>
<h2>Acerca de</h2>
<p>Esta app consume la Rick and Morty API para explorar personajes.</p>
<p>Construida con Vue 3 y Vue Router.</p>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { RouterLink } from 'vue-router'
const personajes = ref([])
const loading = ref(true)
const error = ref(null)
onMounted(async () => {
try {
const respuesta = await fetch('https://rickandmortyapi.com/api/character')
const datos = await respuesta.json()
personajes.value = datos.results
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
})
</script>
<template>
<div>
<h2>Personajes</h2>
<p v-if="loading">Cargando personajes...</p>
<p v-else-if="error" style="color: red">{{ error }}</p>
<div v-else class="grid">
<div v-for="personaje in personajes" :key="personaje.id" class="card">
<img :src="personaje.image" :alt="personaje.name">
<h3>{{ personaje.name }}</h3>
<RouterLink :to="{ name: 'character-detail', params: { id: personaje.id } }">
Ver detalle →
</RouterLink>
</div>
</div>
</div>
</template>
Fíjate cómo en CharacterListView cada tarjeta enlaza a la vista de detalle del personaje. Pero... ¡todavía no hemos definido esa ruta! Vamos a eso.
RouterLink — navegación sin recarga
<RouterLink> es el componente de Vue Router para crear enlaces. A diferencia de un <a href="..."> normal, no recarga la página — simplemente actualiza la URL y renderiza el nuevo componente en el <RouterView />.
Puedes usar to de dos formas:
<!-- Con una ruta string -->
<RouterLink to="/about">Acerca de</RouterLink>
<!-- Con un objeto y el nombre de la ruta (recomendado) -->
<RouterLink :to="{ name: 'about' }">Acerca de</RouterLink>
Usar el nombre de la ruta (:to="{ name: 'about' }") es mejor que escribir la URL directamente. Si algún día cambias la URL /about a /acerca-de, solo lo cambias en el router y todos los enlaces siguen funcionando.
Vue Router añade automáticamente clases CSS a los enlaces activos:
router-link-active— cuando la ruta actual coincide parcialmente (por ejemplo,/characterscoincide con/characters/5).router-link-exact-active— cuando la ruta coincide exactamente.
Construyamos una barra de navegación que aproveche estas clases:
<script setup>
import { RouterLink } from 'vue-router'
</script>
<template>
<nav class="navbar">
<RouterLink :to="{ name: 'home' }">Inicio</RouterLink>
<RouterLink :to="{ name: 'characters' }">Personajes</RouterLink>
<RouterLink :to="{ name: 'about' }">Acerca de</RouterLink>
</nav>
</template>
<style scoped>
.navbar {
display: flex;
gap: 1rem;
padding: 1rem;
background: #1a1a2e;
}
.navbar a {
color: #ccc;
text-decoration: none;
padding: 0.5rem 1rem;
border-radius: 4px;
}
.navbar a.router-link-exact-active {
color: #fff;
background: #4FC08D;
}
</style>
Y actualiza App.vue para incluir la navbar:
<script setup>
import { RouterView } from 'vue-router'
import NavBar from './components/NavBar.vue'
</script>
<template>
<div id="app">
<NavBar />
<main style="padding: 1rem;">
<RouterView />
</main>
</div>
</template>
Rutas dinámicas con parámetros
Las rutas estáticas como /about están bien para páginas fijas. Pero necesitas rutas dinámicas para mostrar contenido según un ID — como el detalle de un personaje específico. Para esto usas parámetros de ruta con :.
Primero, añade la ruta dinámica al router:
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import AboutView from '../views/AboutView.vue'
import CharacterListView from '../views/CharacterListView.vue'
import CharacterDetailView from '../views/CharacterDetailView.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
component: AboutView
},
{
path: '/characters',
name: 'characters',
component: CharacterListView
},
{
path: '/character/:id',
name: 'character-detail',
component: CharacterDetailView
}
]
})
export default router
El :id en /character/:id es un parámetro dinámico. Cuando el usuario visita /character/5, Vue Router captura el 5 y lo hace disponible en el componente.
Ahora crea el componente de detalle — aquí conectamos con el patrón de la lección anterior (consumir APIs):
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRouter, RouterLink } from 'vue-router'
const route = useRoute()
const router = useRouter()
const personaje = ref(null)
const loading = ref(true)
const error = ref(null)
onMounted(async () => {
try {
const respuesta = await fetch(
`https://rickandmortyapi.com/api/character/${route.params.id}`
)
if (!respuesta.ok) {
throw new Error(`Error HTTP: ${respuesta.status}`)
}
personaje.value = await respuesta.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
})
</script>
<template>
<div>
<button @click="router.back()">← Volver</button>
<p v-if="loading">Cargando personaje...</p>
<p v-else-if="error" style="color: red">{{ error }}</p>
<div v-else-if="personaje" class="detalle">
<img :src="personaje.image" :alt="personaje.name">
<h2>{{ personaje.name }}</h2>
<ul>
<li><strong>Estado:</strong> {{ personaje.status }}</li>
<li><strong>Especie:</strong> {{ personaje.species }}</li>
<li><strong>Género:</strong> {{ personaje.gender }}</li>
<li><strong>Origen:</strong> {{ personaje.origin.name }}</li>
<li><strong>Ubicación:</strong> {{ personaje.location.name }}</li>
</ul>
<RouterLink :to="{ name: 'characters' }">
← Volver a la lista
</RouterLink>
</div>
</div>
</template>
Hay dos funciones clave aquí:
useRoute()— te da acceso a la ruta actual: sus parámetros (route.params.id), la query string, el nombre, etc.useRouter()— te da acceso al router para navegar programáticamente (como elrouter.back()del botón "Volver").
Y desde la lista de personajes, enlazas al detalle pasando el ID como parámetro:
<!-- En CharacterListView.vue -->
<RouterLink
:to="{ name: 'character-detail', params: { id: personaje.id } }"
>
Ver detalle →
</RouterLink>
Navegación programática con useRouter()
No siempre navegas con enlaces. A veces necesitas redirigir al usuario desde el código — después de enviar un formulario, al completar una acción, o según una condición. Para esto usas useRouter().
<script setup>
import { useRouter } from 'vue-router'
const router = useRouter()
const irAPersonaje = (id) => {
// Navegar con un objeto (recomendado)
router.push({ name: 'character-detail', params: { id } })
}
const irAInicio = () => {
// Navegar con una string
router.push('/')
}
const volver = () => {
// Volver a la página anterior (como el botón "atrás" del navegador)
router.back()
}
const volverDosPaginas = () => {
// Ir hacia atrás 2 páginas en el historial
router.go(-2)
}
</script>
<template>
<div>
<button @click="irAInicio">Ir al inicio</button>
<button @click="irAPersonaje(42)">Ver personaje 42</button>
<button @click="volver">Volver</button>
<button @click="volverDosPaginas">Volver 2 páginas</button>
</div>
</template>
Los métodos más usados son:
router.push()— navega a una nueva ruta y la añade al historial.router.back()— vuelve una página en el historial (equivale arouter.go(-1)).router.go(n)— avanza o retrocedenpasos en el historial.
Un uso muy común es navegar después de una acción:
const buscarPersonaje = async (nombre) => {
const respuesta = await fetch(
`https://rickandmortyapi.com/api/character/?name=${nombre}`
)
const datos = await respuesta.json()
if (datos.results && datos.results.length === 1) {
// Si solo hay un resultado, ir directo al detalle
router.push({
name: 'character-detail',
params: { id: datos.results[0].id }
})
}
}
Ruta 404 (catch-all)
¿Qué pasa si el usuario visita una URL que no existe, como /xyz? Sin una ruta catch-all, Vue Router simplemente no renderiza nada. Necesitas una ruta que capture todo lo que no coincida con las demás.
// Añade esta ruta AL FINAL del array de rutas
{
path: '/:pathMatch(.*)*',
name: 'not-found',
component: () => import('../views/NotFoundView.vue')
}
El patrón /:pathMatch(.*)* captura cualquier URL que no haya coincidido con las rutas anteriores. Fíjate que también usamos lazy loading con () => import(...) — esto hace que el componente solo se cargue cuando el usuario realmente visita una ruta inválida, optimizando el bundle inicial.
<script setup>
import { RouterLink } from 'vue-router'
</script>
<template>
<div style="text-align: center; padding: 2rem;">
<h2>404 — Página no encontrada</h2>
<p>Esta dimensión no existe en el multiverso.</p>
<RouterLink :to="{ name: 'home' }">Volver al inicio</RouterLink>
</div>
</template>
Resumen
createRouter+createWebHistory— crean el router con historial del navegador (URLs limpias, sin#).<RouterView />— el componente donde se renderiza la vista de la ruta activa.<RouterLink>— navegación sin recarga de página. Usa:to="{ name: '...' }"con rutas nombradas.- Rutas dinámicas —
/character/:idcaptura segmentos de la URL como parámetros, accesibles conuseRoute().params. useRouter()— navegación programática conpush(),back()ygo().- Catch-all —
/:pathMatch(.*)*captura rutas no definidas para mostrar una página 404. - Convención
views/vscomponents/— las páginas van enviews/, los componentes reutilizables encomponents/.
Explorador del Multiverso
Construye un mini explorador de Rick and Morty con Vue Router y 3 rutas principales:
- Home (
/) — página de bienvenida con un enlace para ir a la lista de personajes. - CharacterList (
/characters) — obtiene los personajes de la API (https://rickandmortyapi.com/api/character) y los muestra en tarjetas. Cada tarjeta muestra la imagen, el nombre y el estado del personaje, y enlaza a su vista de detalle. - CharacterDetail (
/character/:id) — obtiene un personaje individual de la API (https://rickandmortyapi.com/api/character/:id) y muestra toda su información: imagen, nombre, estado, especie, género, origen y ubicación actual. Incluye un botón para volver atrás.
Requisitos adicionales:
- Crea un componente
NavBarconRouterLinkpara Home, Personajes y Acerca de. Los enlaces activos deben tener un estilo visual diferente usandorouter-link-exact-active. - Añade una ruta catch-all para mostrar una página 404 con un mensaje creativo y un enlace para volver al inicio.
- Usa
useRoute()para leer los parámetros de la URL yuseRouter()para la navegación programática (botón "volver"). - Implementa el patrón loading/error/data en las vistas que consumen la API.
lightbulb Pistas
Empieza creando el archivo src/router/index.js con todas las rutas definidas. Recuerda que la ruta catch-all (/:pathMatch(.*)*) debe ir al final del array. Para la vista de detalle, usa useRoute().params.id dentro de onMounted para construir la URL de la API. Reutiliza el patrón de tres refs (data, loading, error) que aprendiste en la lección anterior. Si quieres ir más allá, crea un composable useApi() para no repetir la lógica de fetching en cada vista.