Quick Start
import { createRouter } from './build/router.esm.js'
import { mount } from './build/noun.esm.js'
import { Home, About, User } from './pages.js'
const router = createRouter({ mode: 'hash' })
router
.route('/', () => mount('#app', Home))
.route('/about', () => mount('#app', About))
.route('/users/:id', (to) => mount('#app', User, { id: to.params.id }))
.start()
Router Modes
Hash Mode (Default)
URLs like #/users/123
- Works without server config
- Best for static hosting
- No 404 on refresh
History Mode
Clean URLs like /users/123
- Requires server SPA fallback
- Better for SEO
- Use
mode: 'history'
Defining Routes
// Basic routes
router.route('/', () => mount('#app', Home))
router.route('/about', () => mount('#app', About))
// Route parameters
router.route('/users/:id', (to) => {
console.log(to.params.id) // "123" for /users/123
mount('#app', UserDetail, { id: to.params.id })
})
// Multiple parameters
router.route('/users/:userId/posts/:postId', (to) => {
console.log(to.params) // { userId: "1", postId: "5" }
})
// Route with options
router.route('/admin', (to) => mount('#app', Admin), {
name: 'admin',
meta: { requiresAuth: true, roles: ['admin'] },
guards: [adminGuard]
})
// Nested routes
router.route('/dashboard', (to) => mount('#app', Dashboard), {
children: [
{ path: '/overview', handler: () => mount('#content', Overview) },
{ path: '/settings', handler: () => mount('#content', Settings) }
]
})
Navigation Guards
Guards run before navigation completes, allowing access control.
// Global guard
router.guard(async (to, from, next) => {
if (to.meta?.requiresAuth && !isLoggedIn()) {
router.state.redirectAfterAuth = to.path
return next('/login') // Redirect
}
next() // Allow navigation
})
// Role guard
router.guard(async (to, from, next) => {
if (to.meta?.roles) {
const hasRole = to.meta.roles.includes(user?.role)
if (!hasRole) return next('/unauthorized')
}
next()
})
// Helper methods
router.requireAuth(() => authService.isAuthenticated(), '/login')
router.requireRole(() => user.roles, '/unauthorized')
Middleware
Middleware runs after guards pass but cannot cancel navigation. Use it for side effects like analytics, logging, or updating page metadata.
Guards vs Middleware
Guards can block/redirect navigation. Middleware cannot — it only runs side effects after access is granted.
// Basic middleware - runs on every navigation
router.use(async (to, from) => {
console.log(`Navigation: ${from?.path || 'initial'} → ${to.path}`)
})
// Page title middleware
router.use(async (to) => {
document.title = to.meta?.title || 'My App'
})
// Analytics tracking
router.use(async (to, from) => {
analytics.trackPageView({
path: to.path,
referrer: from?.path,
params: to.params
})
})
Real-World Middleware Examples
// Loading indicator middleware
router.use(async (to, from) => {
// Show loading state while page renders
document.body.classList.add('page-loading')
// Remove after a tick (let render complete)
requestAnimationFrame(() => {
document.body.classList.remove('page-loading')
})
})
// Breadcrumb middleware
router.use(async (to) => {
const breadcrumbs = []
const segments = to.path.split('/').filter(Boolean)
let currentPath = ''
for (const segment of segments) {
currentPath += '/' + segment
breadcrumbs.push({
label: segment.charAt(0).toUpperCase() + segment.slice(1),
path: currentPath
})
}
// Update global state or emit event
window.dispatchEvent(new CustomEvent('breadcrumbs', {
detail: breadcrumbs
}))
})
// Session activity tracker
router.use(async (to) => {
// Update last activity timestamp
sessionStorage.setItem('lastActivity', Date.now().toString())
// Extend session if needed
if (to.meta?.requiresAuth) {
await authService.refreshSession()
}
})
Security-Focused Middleware
Use middleware for security logging and monitoring (but use guards for actual access control).
// Security audit logging
router.use(async (to, from) => {
// Log access to sensitive routes
if (to.meta?.sensitive) {
await securityLog.record({
action: 'page_access',
path: to.path,
userId: auth.user?.id,
timestamp: new Date().toISOString(),
previousPath: from?.path
})
}
})
// CSP nonce rotation (for inline scripts)
router.use(async (to) => {
// Generate fresh nonce for each navigation
const nonce = crypto.randomUUID()
document.querySelectorAll('script[data-dynamic]').forEach(s => {
s.nonce = nonce
})
})
// Feature flag middleware
router.use(async (to) => {
// Check if route requires specific feature flags
if (to.meta?.featureFlag) {
const enabled = await featureFlags.isEnabled(to.meta.featureFlag)
if (!enabled) {
// Log attempted access to disabled feature
console.warn(`Feature "${to.meta.featureFlag}" accessed but disabled`)
}
}
})
Events
// Navigation completed
router.on('navigate', ({ to, from }) => {
console.log('Navigated to:', to.path)
})
// Route not found
router.on('notFound', ({ path }) => {
mount('#app', NotFound)
})
// Error in route handler
router.on('error', ({ error, route }) => {
console.error('Route error:', error)
mount('#app', ErrorPage, { error })
})
Security Features
Built-in Protection
The router automatically sanitizes all URLs passed to navigate(), protecting against common client-side attacks.
URL Sanitization
Blocks javascript: and data: URLs to prevent XSS attacks
Path Encoding
Automatic encoding/decoding of path parameters prevents injection attacks
Open Redirect Prevention
External URLs are stripped to path-only, preventing open redirect vulnerabilities
How URL Sanitization Works
// These malicious URLs are blocked:
router.navigate('javascript:alert(1)') // → '/'
router.navigate('data:text/html,<script>') // → '/'
// External URLs are converted to paths (prevents open redirect):
router.navigate('https://evil.com/steal') // → '/steal' (path only)
// Only the path portion is preserved:
router.navigate('https://evil.com/page?token=abc')
// → '/page?token=abc' (origin stripped)
SecurityUtils API
Use these utilities for custom validation in guards and middleware.
import { SecurityUtils } from './build/router.esm.js'
// Sanitize any URL (returns safe path)
const safePath = SecurityUtils.sanitizeUrl(userInput)
// Encode path segments for safe URLs
SecurityUtils.encodePath('/users/John Doe')
// → '/users/John%20Doe'
// Safely decode paths
SecurityUtils.decodePath('/users/John%20Doe')
// → '/users/John Doe'
// Check if a redirect URL is same-origin
if (SecurityUtils.isValidRedirect(redirectUrl)) {
router.navigate(redirectUrl)
} else {
console.warn('Blocked cross-origin redirect')
router.navigate('/')
}
Security Best Practices
Always validate user input
While the router sanitizes paths, always validate route parameters before using them in API calls or database queries.
Use guards for access control
Never rely solely on hiding links. Guards ensure routes are protected even when accessed directly.
Validate redirect targets
When storing redirect paths (like redirectAfterAuth), validate them before navigation.
Server-side validation required
Client-side guards can be bypassed. Always verify permissions server-side for sensitive operations.
Secure Guard Pattern
// Secure authentication guard with redirect validation
router.guard(async (to, from, next) => {
if (to.meta?.requiresAuth && !auth.isAuthenticated()) {
// Store intended destination securely
const safePath = SecurityUtils.sanitizeUrl(to.path)
router.state._redirectAfterAuth = safePath // underscore = private (not in URL)
return next('/login')
}
next()
})
// After login - validate the redirect
async function handleLoginSuccess() {
const redirect = router.state._redirectAfterAuth
delete router.state._redirectAfterAuth
// Validate before navigating
if (redirect && SecurityUtils.isValidRedirect(redirect)) {
router.navigate(redirect)
} else {
router.navigate('/dashboard') // Safe default
}
}
API Reference
| Method | Description |
|---|---|
route(path, handler, options?) | Define a route |
guard(fn) | Add navigation guard |
use(fn) | Add middleware |
start() | Start router |
navigate(path) | Navigate to path |
replace(path) | Replace without history |
back() / forward() / go(n) | History navigation |
on(event, handler) | Listen for event |