Declarative routing for Vue apps. https://rockett.pw/open-source/routier
  • TypeScript 100%
Find a file
Mike Rockétt fc6ce182e4
All checks were successful
Tests / test (push) Successful in 14s
isc license update
2026-04-26 15:25:55 +02:00
.forgejo/workflows v0.1.0-beta.9 2026-04-25 16:33:46 +02:00
lib v0.1.1 2026-04-25 16:40:00 +02:00
tests rename package to @rockett/routier and update urls; add forgejo publish and tests workflows; add jsr.json for jsr publishing; drop WIP notice from readme 2026-04-25 15:49:29 +02:00
.gitignore v0.1.0-beta.8 2026-04-25 16:31:20 +02:00
.oxfmtrc.json add docs and quick start to README; switch from prettier to oxfmt and adopt oxlint; remove vitepress docs and cached deps; ignore bun.lock and update deps 2026-04-25 14:12:58 +02:00
.oxlintrc.json add docs and quick start to README; switch from prettier to oxfmt and adopt oxlint; remove vitepress docs and cached deps; ignore bun.lock and update deps 2026-04-25 14:12:58 +02:00
bun.lock v0.1.0-beta.8 2026-04-25 16:31:20 +02:00
jsr.json v0.1.1 2026-04-25 16:40:00 +02:00
LICENSE.md isc license update 2026-04-26 15:25:55 +02:00
package.json v0.1.1 2026-04-25 16:40:00 +02:00
README.md isc license update 2026-04-26 15:13:05 +02:00
tsconfig.json fix guard rejection handling to check loop after compiling rejection; remove demo app; expand factory and route tests, add guards test; switch build to esm-only with vue-router external; tighten guard handle types 2026-04-25 14:45:54 +02:00
vite.config.ts fix guard rejection handling to check loop after compiling rejection; remove demo app; expand factory and route tests, add guards test; switch build to esm-only with vue-router external; tighten guard handle types 2026-04-25 14:45:54 +02:00

Routier

License: ISC npm version JSR TypeScript

Fluent, function-based route definitions for Vue Router. Inspired by Laravel's routing.

Yes, yes - file-based routing is all the hype. This package is for those of us who don't want that, but still want a better DX than raw Vue Router config.

Installation

Routier requires Vue Router 4+.

npm install @rockett/routier vue-router
# or
pnpm add @rockett/routier vue-router
# or
bun add @rockett/routier vue-router

Quick Start

import { createRouteFactory, Guard } from '@rockett/routier'
import { createRouter, createWebHistory } from 'vue-router'

import Home from './views/Home.vue'
import Blog from './views/Blog.vue'
import BlogPost from './views/BlogPost.vue'
import Dashboard from './views/Dashboard.vue'

class AuthGuard extends Guard {
  handle(resolve, reject) {
    if (isAuthenticated()) resolve()
    else reject({ name: 'home' })
  }
}

const { route, redirect, fallback, group, compileRoutes } = createRouteFactory({
  guards: { auth: AuthGuard },
})

route('/', Home).name('home')

group('blog')
  .name('blog')
  .routes(() => {
    route('/', Blog).name('index')
    route('{slug}', BlogPost).name('post')
  })

group('dashboard')
  .name('dashboard')
  .guard('auth')
  .routes(() => {
    route('/', Dashboard).name('home')
  })

redirect('/old-blog', '/blog')
fallback(NotFound).name('not-found')

const router = createRouter({
  history: createWebHistory(),
  routes: compileRoutes(),
})

API

createRouteFactory(opts?)

Creates a route factory. Returns { route, redirect, group, compileRoutes, dump }.

const { route, redirect, group, compileRoutes } = createRouteFactory({
  separateNamesUsing: '.', // name separator: '.' | '-' | '_' | ':' | '>'
  resolveComponentsUsing: (name) => import(`./views/${name}.vue`),
  guards: { auth: AuthGuard },
})

The resolveComponentsUsing option lets you pass strings instead of imported components:

route('dashboard', 'Dashboard') // resolved via the callback above

Routes

route(path, component, additionalComponents?)

Routes are the primary building block. The fluent API lets you chain configuration:

route('users/{id}', UserProfile)
  .name('users.profile')
  .alias('/people/{id}')
  .alias('/u/{id}')
  .meta('requiresAuth', true)
  .props(true)
  .guard('auth')
  .sensitive()
  .strict()

Props support three modes matching Vue Router's options:

route('/', Home).props(true) // boolean mode
route('/', Home).props({ defaultTab: 'overview' }) // object mode
route('/', Home).props((to) => ({ q: to.query.q })) // function mode
route('/', Home).prop('key', 'value') // single key-value

Children nest routes via a callback:

route('account', Account)
  .name('account')
  .children(() => {
    route('/', AccountOverview).name('overview')
    route('settings', AccountSettings).name('settings')
  })

Aliases can be chained to register multiple alternative paths for a single route:

route('/users', Users).alias('/people').alias('/utilisateurs')

A single alias compiles to a string; multiple aliases compile to an array, matching Vue Router's format.

Matching options

sensitive makes path matching case-sensitive (by default /Users and /users match the same route). strict enforces trailing-slash matching (by default /users/ and /users are equivalent).

Both can be set at three levels:

// factory-level: applies to all routes
const { route, group } = createRouteFactory({ sensitive: true, strict: true })

// group-level: applies to all routes in the group
group('api').sensitive().strict().routes(() => { ... })

// route-level
route('/users', Users).sensitive().strict()

Path parameters

Routier supports Vue Router's :param syntax directly, and also provides a curly-brace syntax with optional type hints:

Syntax Compiles to Description
:id :id Standard Vue Router param
{id} :id Curly-brace shorthand
{id}(number) :id(\d+) Numeric constraint
{name}(string) :name(\w+) Word-character constraint
{all} (.*) Wildcard / catch-all
{a}{b} :a/:b Consecutive params expand to segments

Groups

Groups let you share a path prefix, name prefix, and guards across a set of routes - a concept borrowed from Laravel that Vue Router doesn't have natively. Any combination of prefix, name, and guards is optional.

group(prefix?)
  .name(name)
  .guard(...names)
  .routes(() => { ... })

Prefix only - shared path segment:

group('api/v2').routes(() => {
  route('users', Users) // /api/v2/users
  route('posts', Posts) // /api/v2/posts
})

Name only - shared name prefix without affecting paths:

group()
  .name('admin')
  .routes(() => {
    route('dashboard', Dashboard).name('home') // name: admin.home
  })

Guards - protect all routes in the group:

group('admin')
  .name('admin')
  .guard('auth')
  .routes(() => {
    route('/', AdminDashboard).name('dashboard')
    route('users', AdminUsers).name('users')
  })

Groups nest freely, including inside children():

group('app')
  .name('app')
  .routes(() => {
    route('dashboard', Dashboard)
      .name('dashboard')
      .children(() => {
        group('widgets')
          .name('widgets')
          .routes(() => {
            route('chart', Chart).name('chart') // name: app.dashboard.widgets.chart
            route('table', Table).name('table') // path: widgets/table
          })
      })
  })

Redirects

redirect(path, target).name(name)

Redirect routes compile to Vue Router's redirect record (no component). The target can be a path string, a route location object, or a function:

redirect('/old', '/new')
redirect('/old', { name: 'new-route' })
redirect('/old/{slug}', (to) => `/new/${to.params.slug}`)

Redirects work inside groups and as children, inheriting prefixes and names:

group('legacy').routes(() => {
  redirect('old-page', '/new-page') // /legacy/old-page -> /new-page
})

Fallback

fallback(component).name(name)

Sugar for a catch-all route (/:pathMatch(.*)*). At the top level, it's always compiled last regardless of declaration order. Supports .name() and .meta():

fallback(NotFound).name('not-found').meta('status', 404)

Fallbacks also work inside groups and children for scoped catch-alls:

route('/app', App).children(() => {
  route('dashboard', Dashboard)
  fallback(SectionNotFound) // catches unmatched /app/* paths
})

Guards

Guards are class-based navigation guards using a promise-style resolve/reject pattern. Define a guard by extending the Guard class:

import { Guard } from '@rockett/routier'

class AuthGuard extends Guard {
  handle(resolve, reject, { from, to }) {
    if (isAuthenticated()) resolve()
    else reject({ name: 'login' })
  }
}

Register guards in the factory, then reference them by name:

const { route, group } = createRouteFactory({
  guards: { auth: AuthGuard, admin: AdminGuard },
})

route('/dashboard', Dashboard).guard('auth')
group('admin').guard('auth', 'admin').routes(() => { ... })

The reject() value is passed to Vue Router's next() - typically a route location to redirect to. If a rejection would redirect to the same route being navigated to, Routier throws a GuardError to prevent infinite loops.

Set loggable = true on a guard to log resolve/reject events to the console during development.

Full Example

route('blog', Blog)
  .name('blog')
  .children(() => {
    route('/', BlogPosts).name('posts')

    route('{post}', BlogPost)
      .name('single-post')
      .children(() => {
        route('/', BlogPostView).name('view')
        route('comments', BlogPostComments).name('comments')

        group('admin')
          .guard('admin')
          .name('admin')
          .routes(() => {
            route('edit', BlogPostEdit).name('edit')
            route('stats', BlogPostStats).name('stats')
          })
      })
  })

This produces:

Path Name Guard
/blog blog.posts
/blog/:post blog.single-post.view
/blog/:post/comments blog.single-post.comments
/blog/:post/admin/edit blog.single-post.admin.edit admin
/blog/:post/admin/stats blog.single-post.admin.stats admin

License

Licensed under ISC, Routier is an open-source project, and is free to use.