This guide covers everything you need to integrate @esmx/router with Vue 3. By the end, you'll have a fully working Vue 3 app with routing and server-side rendering (SSR).
Install the core router and the Vue integration package:
npm install @esmx/router @esmx/router-vue@esmx/router is the framework-agnostic router core. @esmx/router-vue provides Vue-specific bindings — a plugin, composables, and components.
The Vue 3 integration relies on three pieces:
RouterView and RouterLink as global componentsprovide/injectCreate a single source of truth for route definitions:
import type { RouteConfig } from '@esmx/router';
export const routes: RouteConfig[] = [
{
path: '/',
component: () => import('./layouts/MainLayout.vue'),
children: [
{ path: '', component: () => import('./pages/Home.vue') },
{ path: 'about', component: () => import('./pages/About.vue') },
{
path: 'users/:id',
component: () => import('./pages/UserProfile.vue'),
meta: { requiresAuth: true }
}
]
}
];A shared factory that both the client and server entries use:
import { h, createApp, createSSRApp } from 'vue';
import { Router } from '@esmx/router';
import { RouterPlugin, useProvideRouter } from '@esmx/router-vue';
import App from './App.vue';
export function createVueApp(router: Router, ssr = false) {
const create = ssr ? createSSRApp : createApp;
const app = create({
setup() {
useProvideRouter(router);
return () => h(App);
}
});
app.use(RouterPlugin);
return { app, router };
}Key points:
useProvideRouter(router) must be called inside setup() of the root component. It makes the router available to all descendants via useRouter() and useRoute().RouterPlugin registers RouterView and RouterLink as global components, and sets up $router and $route on globalProperties.createSSRApp for server-side rendering and createApp for client-side only.The client entry creates the router in history mode and mounts the app:
import { Router, RouterMode } from '@esmx/router';
import { createVueApp } from './create-app';
import { routes } from './routes';
const router = new Router({
root: '#app',
mode: RouterMode.history,
routes
});
const { app } = createVueApp(router);
app.mount('#app');RouterMode.history uses the browser's History API (pushState, popstate) for clean URLs.
The server entry creates the router in memory mode and renders HTML:
import type { RenderContext } from '@esmx/core';
import { renderToString } from '@vue/server-renderer';
import { Router, RouterMode } from '@esmx/router';
import { createVueApp } from './create-app';
import { routes } from './routes';
export default async (rc: RenderContext) => {
const router = new Router({
mode: RouterMode.memory,
base: new URL(rc.params.url, 'http://localhost'),
routes
});
await router.replace(rc.params.url);
const { app } = createVueApp(router, true);
const html = await renderToString(app);
rc.html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
${rc.preload()}
${rc.css()}
</head>
<body>
<div id="app">${html}</div>
${rc.importmap()}
${rc.moduleEntry()}
${rc.modulePreload()}
</body>
</html>`;
};RouterMode.memory keeps the routing state in memory — no browser API calls, perfect for Node.js environments.
The Node entry configures the dev server and build tooling:
import http from 'node:http';
import type { EsmxOptions } from '@esmx/core';
export default {
async devApp(esmx) {
return import('@esmx/rspack-vue').then((m) =>
m.createRspackVue3App(esmx)
);
},
async server(esmx) {
const server = http.createServer((req, res) => {
esmx.middleware(req, res, async () => {
const rc = await esmx.render({
params: { url: req.url }
});
res.end(rc.html);
});
});
server.listen(3000, () => {
console.log('Server started: http://localhost:3000');
});
}
} satisfies EsmxOptions;createRspackVue3App from @esmx/rspack-vue sets up Rspack with Vue 3 SFC support, HMR, and SSR bundling.
RouterView renders the matched component for the current route. RouterLink creates navigation links with active state management:
<template>
<div>
<nav>
<RouterLink to="/">Home</RouterLink>
<RouterLink to="/about">About</RouterLink>
<RouterLink to="/users/42">User 42</RouterLink>
</nav>
<RouterView />
</div>
</template>RouterLink automatically applies the router-link-active class when the current route matches the link's to path. Customize it with the activeClass prop:
<RouterLink to="/about" activeClass="nav-active">About</RouterLink>Access the router and current route inside <script setup>:
<template>
<div>
<h1>User {{ route.params.id }}</h1>
<p>Current path: {{ route.path }}</p>
<p>Query: {{ JSON.stringify(route.query) }}</p>
<p>Meta: {{ JSON.stringify(route.meta) }}</p>
<button @click="goHome">Go Home</button>
<button @click="goToNextUser">Next User</button>
</div>
</template>
<script setup lang="ts">
import { useRouter, useRoute } from '@esmx/router-vue';
import { watch } from 'vue';
const router = useRouter();
const route = useRoute();
function goHome() {
router.push('/');
}
function goToNextUser() {
const currentId = Number(route.params.id);
router.push(`/users/${currentId + 1}`);
}
// Watch for route changes
watch(() => route.path, (newPath, oldPath) => {
console.log(`Route changed from ${oldPath} to ${newPath}`);
});
</script>The router provides several navigation methods:
<script setup lang="ts">
import { useRouter } from '@esmx/router-vue';
const router = useRouter();
// Push a new entry onto the history stack
router.push('/about');
// Replace the current entry (no new history entry)
router.replace('/about');
// Go back
router.back();
// Go forward
router.forward();
// Go to a specific history offset
router.go(-2);
</script>The route object provides full information about the current route:
<script setup lang="ts">
import { useRoute } from '@esmx/router-vue';
const route = useRoute();
// Current path
route.path // '/users/42'
// Route parameters
route.params // { id: '42' }
// Query string parameters
route.query // { tab: 'profile' } for /users/42?tab=profile
// Route meta data
route.meta // { requiresAuth: true }
// Full URL object
route.url // URL instance
// Matched route configs (parent → child)
route.matched // RouteConfig[]
</script>A typical Vue 3 + SSR project with @esmx/router:
src/
├── entry.node.ts # Node.js server setup, dev/build config
├── entry.server.ts # SSR rendering logic
├── entry.client.ts # Client-side hydration/mounting
├── create-app.ts # Shared app factory (used by both server & client)
├── routes.ts # Route definitions
├── App.vue # Root component (RouterView + navigation)
├── layouts/
│ └── MainLayout.vue # Layout with nested RouterView
└── pages/
├── Home.vue
├── About.vue
└── UserProfile.vue