A TypeScript-first SvelteKit library for type-safe URL generation and route parameter validation using Standard Schema.
- Installation
- Features
- Quick Start
- Advanced Usage
- Vite Plugin Architecture
- API Reference
- Migration Guide
- Contributing
npm install skroutes
# or
pnpm add skroutes
- 🔄 Vite Plugin Integration: Automatically discovers routes from your SvelteKit file structure
- 🎯 Zero Manual Configuration: No need to manually register routes - just add
_routeConfig
exports - ⚡ Hot Reload: Instant type updates when you modify route files
- 🏗️ Smart Type Generation: Generates precise TypeScript types from your validation schemas
- 🌍 Dual Config System: Optimized client/server configurations for different environments
- 🏷️ Standard Schema Support: Works with Zod, Valibot, ArkType, and any Standard Schema-compliant library
- 📝 Type-safe URL generation with automatic validation and proper return types
- 🚦 Compile-time Route Validation: TypeScript catches invalid routes and parameter types
- 🎯 Parameter Strategy System: Flexible handling of unconfigured routes (
allowAll
,never
,simple
,strict
,deriveParams
) - 🎯 Non-Optional Results:
params
andsearchParams
are never undefined - no optional chaining needed
- 🛠️ Easy URL manipulation with strongly typed parameter updates
- 🔄 Bi-directional synchronization: Changes flow seamlessly between URL state and component state
- ⚡ Throttled updates: Built-in throttling prevents excessive URL changes during rapid state updates
- 🎯 Direct binding: Bind form inputs directly to URL parameters with automatic synchronization
- 💾 Unsaved changes detection: Track when internal state differs from URL state
- 🔄 Reset functionality: Easily revert changes back to the current URL state
- 🎨 Svelte 5 Runes Support: Full compatibility with modern Svelte reactive patterns
- 📊 Debug mode: Comprehensive logging to understand state synchronization flow
- 🔧 Flexible Configuration: Extensive plugin options for customization
- 📁 Clean File Organization: Co-locate validation with route logic
// src/lib/routes.ts
import { skRoutes } from 'skroutes';
import { z } from 'zod';
export const { urlGenerator, pageInfo, loadConfig } = skRoutes({
config: async () => ({
'/users/[id]': {
paramsValidation: z.object({
id: z.string().uuid()
}),
searchParamsValidation: z.object({
tab: z.enum(['profile', 'settings']).optional(),
page: z.coerce.number().positive().optional()
})
},
'/products/[slug]': {
paramsValidation: z.object({
slug: z.string().min(1)
})
}
}),
errorURL: '/error'
});
Since the configuration is now asynchronous, you must call loadConfig()
before using any other exported functions. Use the SvelteKit hooks files for proper initialization:
// src/hooks.server.ts
import { loadConfig } from '$lib/routes';
// Initialize the configuration before handling any requests
await loadConfig();
export async function handle({ event, resolve }) {
// Your handle logic here
return resolve(event);
}
// src/hooks.client.ts
import { loadConfig } from '$lib/routes';
// Initialize the configuration on the client
await loadConfig();
urlGenerator
, pageInfo
, and other skRoutes functions will not work correctly until loadConfig()
has been awaited. The hooks files are the recommended location for this initialization as they run before your application starts handling requests or rendering pages.
import { urlGenerator } from '$lib/routes';
// Generate a URL with validation - all parameters are strongly typed!
const userUrl = urlGenerator({
address: '/users/[id]', // ✅ TypeScript validates this route exists
paramsValue: { id: 'user123' }, // ✅ TypeScript knows id: string is required
searchParamsValue: { tab: 'profile', page: 1 } // ✅ TypeScript validates tab and page types
});
console.log(userUrl.url); // '/users/user123?tab=profile&page=1'
console.log(userUrl.error); // false
console.log(userUrl.params); // ✅ Typed as { id: string } (never undefined!)
console.log(userUrl.searchParams); // ✅ Typed as { tab: 'profile' | 'settings' | undefined, page: number | undefined } (never undefined!)
// No need for optional chaining - params and searchParams are guaranteed to exist
const userId = userUrl.params.id; // ✅ Direct access, no userUrl.params?.id needed
const userTab = userUrl.searchParams.tab; // ✅ Direct access, no userUrl.searchParams?.tab needed
// ❌ TypeScript will catch these errors at compile time:
// urlGenerator({ address: '/nonexistent' }); // Error: route doesn't exist
// urlGenerator({ address: '/users/[id]', paramsValue: { id: 123 } }); // Error: id must be string
// urlGenerator({ address: '/users/[id]', searchParamsValue: { tab: 'invalid' } }); // Error: invalid tab value
<!-- src/routes/users/[id]/+page.svelte -->
<script lang="ts">
import { pageInfo } from '$lib/routes';
import { page } from '$app/state';
export let data; // From your +page.server.ts
// Create reactive route info with throttled URL updates
const route = pageInfo('/users/[id]', () => $page, {
updateDelay: 500, // 500ms throttling
onUpdate: (url) => console.log('URL updated:', url),
debug: false // Enable for debugging
});
const tabs = ['profile', 'settings'] as const;
// Function to switch tabs
function switchTab(tab: string) {
route.updateParams({ searchParams: { tab } });
}
// You can also bind directly to form inputs
let searchQuery = $state('');
// Bind to route parameters for reactive URL updates
$effect(() => {
if (searchQuery) {
route.current.searchParams = {
...route.current.searchParams,
query: searchQuery
};
}
});
</script>
<div class="page">
<h1>User: {route.current.params.id}</h1>
<p>Current Tab: {route.current.searchParams.tab || 'profile'}</p>
<!-- Show unsaved changes indicator -->
{#if route.hasChanges}
<div class="unsaved-changes">
<p>You have unsaved changes</p>
<button onclick={route.resetParams}>Reset</button>
</div>
{/if}
<!-- Direct binding to search parameters -->
<input type="text" bind:value={route.current.searchParams.query} placeholder="Search..." />
<div class="tabs">
{#each tabs as tab}
<button onclick={() => switchTab(tab)} class:active={route.current.searchParams.tab === tab}>
{tab}
</button>
{/each}
</div>
</div>
The Vite plugin is the recommended approach for using skRoutes. It automatically scans your SvelteKit route files and generates fully-typed configurations with zero manual setup.
// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { skRoutesPlugin } from 'skroutes/plugin';
export default defineConfig({
plugins: [
sveltekit(),
skRoutesPlugin({
// Required: URL to redirect to when validation fails
errorURL: '/error',
// Optional: Directory containing SvelteKit routes (relative to project root)
routesDirectory: 'src/routes', // default: 'src/routes'
// Optional: Additional imports for your validation schemas
imports: ["import { z } from 'zod';"],
// Optional: How to handle routes without explicit validation
unconfiguredParams: 'deriveParams', // 'allowAll' | 'never' | 'simple' | 'strict' | 'deriveParams' (default)
unconfiguredSearchParams: 'never', // 'allowAll' | 'never' | 'simple' | 'strict' (default)
// Optional: Custom paths for generated files
clientOutputPath: 'src/lib/.generated/skroutes-client-config.ts',
serverOutputPath: 'src/lib/.generated/skroutes-server-config.ts',
// Optional: Include server-side files in scanning
includeServerFiles: true,
// Optional: Configure which files to scan
serverFiles: ['+page.server.ts', '+server.ts', '+page.server.js', '+server.js'], // default
clientFiles: ['+page.ts', '+page.js'], // default
// Optional: Configure the target variable name to search for
targetVariable: '_routeConfig', // default
// Optional: Manual route configs to include
baseConfig: {
'/api/health': {
paramsValidation: undefined,
searchParamsValidation: undefined
}
}
})
]
});
Add validation to your route files using the configured target variable (default: _routeConfig
):
// src/routes/users/[id]/+page.ts
import { z } from 'zod';
// Export validation configuration
export const _routeConfig = {
paramsValidation: z.object({
id: z.string().uuid()
}).parse,
searchParamsValidation: z.object({
tab: z.enum(['profile', 'settings']).optional(),
page: z.coerce.number().positive().optional()
}).parse
};
// Your normal load function
export async function load({ params, url }) {
// params.id is automatically validated as UUID
// url.searchParams are automatically validated
return {
user: await fetchUser(params.id)
};
}
// src/routes/products/[category]/+page.server.ts
import { z } from 'zod';
export const _routeConfig = {
paramsValidation: z.object({
category: z.enum(['electronics', 'clothing', 'books'])
}).parse,
searchParamsValidation: z.object({
sort: z.enum(['price', 'name', 'rating']).optional(),
minPrice: z.coerce.number().min(0).optional(),
maxPrice: z.coerce.number().min(0).optional()
}).parse
};
export async function load({ params, url }) {
// Fully typed and validated parameters
return {
products: await fetchProducts(params.category, {
sort: url.searchParams.get('sort'),
minPrice: url.searchParams.get('minPrice'),
maxPrice: url.searchParams.get('maxPrice')
})
};
}
The plugin generates two configuration files that you can import using async config loading:
// src/lib/routes.ts - Using auto-generated client config
import { skRoutes } from 'skroutes';
export const { urlGenerator, pageInfo, loadConfig } = skRoutes({
config: async () => {
const { clientRouteConfig } = await import('$lib/.generated/skroutes-client-config');
return clientRouteConfig;
},
errorURL: '/error'
});
// For server-side usage
export const { urlGeneratorServer, serverPageInfo, loadConfigServer } = skRoutesServer({
config: async () => {
const { serverRouteConfig } = await import('$lib/.generated/skroutes-server-config');
return serverRouteConfig;
},
errorURL: '/error'
});
// Initialize configurations in hooks files:
// src/hooks.server.ts: await loadConfig(); await loadConfigServer();
// src/hooks.client.ts: await loadConfig();
// All your routes are automatically typed and available!
const userUrl = urlGenerator({
address: '/users/[id]', // ✅ Auto-completed from your route files
paramsValue: { id: 'user123' }, // ✅ TypeScript knows this must be a UUID
searchParamsValue: { tab: 'profile' } // ✅ TypeScript validates enum values
});
// Perfect type inference for all discovered routes
const productUrl = urlGenerator({
address: '/products/[category]',
paramsValue: { category: 'electronics' }, // ✅ Only allows valid enum values
searchParamsValue: { sort: 'price', minPrice: 50 }
});
The plugin provides flexible strategies for handling routes without explicit validation:
skRoutesPlugin({
// Strategy for route parameters [id], [slug], etc.
unconfiguredParams: 'never', // No parameters allowed
// unconfiguredParams: 'allowAll', // Accept any string parameters
// unconfiguredParams: 'simple', // Optional string parameters
// unconfiguredParams: 'strict', // Compile-time error (prevents usage)
// unconfiguredParams: 'deriveParams', // Auto-derive from route path (NEW!)
// Strategy for search parameters ?page=1&sort=name
unconfiguredSearchParams: 'simple', // Optional string/array parameters
// unconfiguredSearchParams: 'never', // No search parameters allowed
// unconfiguredSearchParams: 'allowAll', // Accept any search parameters
// unconfiguredSearchParams: 'strict', // Compile-time error (prevents usage)
errorURL: '/error'
});
Strategy Examples:
'never'
: Routes generate{}
types - no unconfigured parameters allowed'allowAll'
: Routes generateRecord<string, string>
- accepts any parameters'simple'
: Routes generate{ [key: string]?: string }
- optional parameters'strict'
: Routes generatenever
- TypeScript prevents usage entirely'deriveParams'
(NEW!): Automatically derives exact parameter types from route paths/users/[id]
→{ id: string }
/posts/[slug]/comments/[[page]]
→{ slug: string; page?: string }
/products
→{}
(no parameters)
Why use deriveParams
?
// With 'deriveParams', you get automatic type safety without manual configuration
// Route: /users/[id]/posts/[[page]]
const userPostsUrl = urlGenerator('/users/[id]/posts/[[page]]', {
params: {
id: '123', // ✅ Required - TypeScript enforces this
page: '2' // ✅ Optional - inferred from [[page]]
}
});
// TypeScript catches errors automatically:
const badUrl = urlGenerator('/users/[id]/posts/[[page]]', {
params: {
// ❌ Error: Property 'id' is missing - TypeScript catches this!
page: '2'
}
});
The plugin automatically watches your route files and regenerates configurations when:
- You add/remove route files
- You modify
_routeConfig
exports - You change validation schemas
This provides seamless development experience with instant TypeScript feedback.
skRoutes uses a sophisticated bi-directional synchronization system that keeps URL state and component state in perfect sync:
const route = pageInfo('/search', () => $page, {
updateDelay: 300, // Throttle URL updates to 300ms
debug: true // See synchronization in action
});
// Changes to internal state are throttled before updating the URL
route.current.searchParams.query = 'hello';
route.current.searchParams.filter = 'active';
// ↑ Both changes are batched and applied after 300ms
// External URL changes (like browser navigation) immediately sync to internal state
// No throttling on incoming changes - only outgoing URL updates are throttled
Key Benefits:
- Smooth UX: Rapid typing in search boxes doesn't create browser history spam
- Batched Updates: Multiple parameter changes are combined into single URL updates
- Instant Sync: External navigation immediately updates component state
- Change Detection: Only real content changes trigger updates (not just object reference changes)
<script>
const route = pageInfo('/products', () => $page, {
updateDelay: 500,
debug: false
});
// Complex form state that syncs to URL
let formData = $state({
search: route.current.searchParams.search || '',
category: route.current.searchParams.category || 'all',
priceRange: [
Number(route.current.searchParams.minPrice) || 0,
Number(route.current.searchParams.maxPrice) || 1000
]
});
// Sync form changes back to URL (throttled)
$effect(() => {
route.current.searchParams = {
search: formData.search || undefined,
category: formData.category !== 'all' ? formData.category : undefined,
minPrice: formData.priceRange[0] !== 0 ? formData.priceRange[0].toString() : undefined,
maxPrice: formData.priceRange[1] !== 1000 ? formData.priceRange[1].toString() : undefined
};
});
</script>
<!-- Form inputs automatically stay in sync with URL -->
<input bind:value={formData.search} placeholder="Search products..." />
<select bind:value={formData.category}>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="clothing">Clothing</option>
</select>
<!-- Show unsaved changes -->
{#if route.hasChanges}
<div class="changes-indicator">
<span>Unsaved filters</span>
<button onclick={route.resetParams}>Reset</button>
</div>
{/if}
You can also manually configure routes without the plugin using async config loading:
// src/lib/routes.ts
import { skRoutes } from 'skroutes';
import { z } from 'zod';
export const { urlGenerator, pageInfo, loadConfig } = skRoutes({
config: async () => ({
'/users/[id]': {
paramsValidation: z.object({ id: z.string().uuid() }),
searchParamsValidation: z.object({
tab: z.enum(['profile', 'settings']).optional(),
search: z.string().optional(),
page: z.coerce.number().positive().optional()
})
},
'/products/[category]': {
paramsValidation: z.object({
category: z.enum(['electronics', 'clothing', 'books'])
}),
searchParamsValidation: z.object({
sort: z.enum(['price', 'name', 'rating']).optional(),
minPrice: z.coerce.number().min(0).optional(),
maxPrice: z.coerce.number().min(0).optional()
})
}
}),
errorURL: '/error',
updateAction: 'goto' // Default behavior for all pageInfo instances
});
// Remember to initialize in hooks files:
// src/hooks.server.ts: await loadConfig();
// src/hooks.client.ts: await loadConfig();
Breaking Circular Dependencies:
The async config pattern is especially useful for breaking circular dependencies:
// This prevents circular imports between your route files and skRoutes
export const { urlGenerator, pageInfo, loadConfig } = skRoutes({
config: async () => {
// Dynamic import prevents circular dependency
const routeConfigs = await import('./my-route-configs');
return routeConfigs.default;
},
errorURL: '/error'
});
// Don't forget to initialize in hooks files:
// src/hooks.server.ts: await loadConfig();
// src/hooks.client.ts: await loadConfig();
When validation fails, skRoutes redirects to your configured errorURL
:
const result = urlGenerator({
address: '/users/[id]',
paramsValue: { id: 'invalid-uuid' }
});
if (result.error) {
console.log(result.url); // '/error?message=Error+generating+URL'
}
Works with any Standard Schema-compliant validation library:
// Zod
import { z } from 'zod';
const zodSchema = z.object({ id: z.string() });
// Valibot
import * as v from 'valibot';
const valibotSchema = v.object({ id: v.string() });
// ArkType
import { type } from 'arktype';
const arkSchema = type({ id: 'string' });
// Use any of these in your route config
export const routes = skRoutes({
config: async () => ({
'/users/[id]': {
paramsValidation: zodSchema // or valibotSchema, or arkSchema
}
}),
errorURL: '/error'
});
The skRoutes Vite plugin provides a sophisticated auto-generation system that eliminates manual configuration:
src/routes/
├── +page.ts → '/' route
├── users/[id]/
│ ├── +page.ts → '/users/[id]' route
│ └── +page.server.ts → '/users/[id]' server config
├── products/[category]/
│ ├── +page.svelte → '/products/[category]' route
│ └── +server.ts → '/products/[category]' API endpoint
└── api/health/
└── +server.ts → '/api/health' API route
The plugin automatically scans your src/routes
directory and discovers:
- Page routes (
+page.ts
,+page.svelte
) - Server routes (
+page.server.ts
,+server.ts
) - API endpoints (
+server.ts
) - Route parameters from directory structure (
[id]
,[[slug]]
)
The plugin looks for _routeConfig
exports in your route files:
// Detected automatically ✅
export const _routeConfig = {
paramsValidation: z.object({ id: z.string() }).parse,
searchParamsValidation: z.object({ tab: z.string().optional() }).parse
};
// Also detects partial configs ✅
export const _routeConfig = {
paramsValidation: z.object({ id: z.string() }).parse
// searchParamsValidation will use the configured strategy
};
// Routes without _routeConfig use configured strategies ✅
For each route, the plugin generates appropriate types based on:
Explicit Configuration: Uses your validation schemas for precise typing
// Your schema
z.object({ id: z.string().uuid() });
// Generated type
{
id: string;
} // with UUID validation at runtime
Route Parameters: Automatically detects from file structure
// File: src/routes/posts/[slug]/comments/[id]/+page.ts
// Generated type
{ slug: string; id: string }
// File: src/routes/blog/[[year]]/+page.ts
// Generated type
{ year?: string } // Optional parameter
Configured Strategies: Uses your strategy for unconfigured routes
// Strategy: 'never'
// Generated type for routes without _routeConfig
{
params: {
}
searchParams: {
}
}
// Strategy: 'allowAll'
// Generated type for routes without _routeConfig
{
params: Record<string, string>;
searchParams: Record<string, unknown>;
}
The plugin generates two optimized configuration files:
Client Config (skroutes-client-config.ts
):
- Safe for browser environments
- Only imports from client-side files (
+page.ts
) - Type-only imports for server schemas (for better inference)
- Smaller bundle size
Server Config (skroutes-server-config.ts
):
- Full access to all routes
- Imports from both client and server files
- Complete validation coverage
- Used for server-side rendering
graph TD
A[Save route file] --> B[Plugin detects change]
B --> C[Scan for _routeConfig]
C --> D[Generate types & validation]
D --> E[Update config files]
E --> F[TypeScript re-checks]
F --> G[Instant feedback]
Hot Reload: Changes are detected instantly and configs are regenerated Type Safety: Immediate TypeScript feedback in your editor Zero Config: No manual route registration needed
interface PluginOptions {
/** URL to redirect to when validation fails */
errorURL: string;
/** Path for server-side config file */
serverOutputPath?: string; // default: 'src/lib/.generated/skroutes-server-config.ts'
/** Path for client-side config file */
clientOutputPath?: string; // default: 'src/lib/.generated/skroutes-client-config.ts'
/** Additional import statements for generated files */
imports?: string[]; // default: []
/** Include server files (+page.server.ts, +server.ts) in scanning */
includeServerFiles?: boolean; // default: true
/** Manual route configurations to include */
baseConfig?: Record<string, any>; // default: {}
/** Directory containing SvelteKit routes relative to project root */
routesDirectory?: string; // default: 'src/routes'
/** Strategy for handling unconfigured route parameters */
unconfiguredParams?: 'allowAll' | 'never' | 'simple' | 'strict' | 'deriveParams'; // default: 'deriveParams'
/** Strategy for handling unconfigured search parameters */
unconfiguredSearchParams?: 'allowAll' | 'never' | 'simple' | 'strict'; // default: 'never'
/** Server-side files to scan for route configurations */
serverFiles?: string[]; // default: ["+page.server.ts", "+server.ts", "+page.server.js", "+server.js"]
/** Client-side files to scan for route configurations */
clientFiles?: string[]; // default: ["+page.ts", "+page.js"]
/** Target variable name to search for in route files */
targetVariable?: string; // default: "_routeConfig"
}
// Default configuration (recommended for most projects)
unconfiguredParams: 'deriveParams', // Auto-derives exact parameters from route paths (NEW DEFAULT!)
unconfiguredSearchParams: 'never' // Forces explicit search param configuration (NEW DEFAULT!)
// For new projects - strict validation
unconfiguredParams: 'strict', // Prevents accidental usage
unconfiguredSearchParams: 'never', // Forces explicit configuration
// For existing projects - gradual adoption
unconfiguredParams: 'simple', // Allows migration
unconfiguredSearchParams: 'simple', // Optional parameters
// For maximum flexibility
unconfiguredParams: 'allowAll', // Accepts any parameters
unconfiguredSearchParams: 'allowAll' // Accepts any search params
// ✅ Keep validation schemas close to usage
// src/routes/users/[id]/+page.ts
import { userSchema } from './schemas'; // Local schema file
export const _routeConfig = {
paramsValidation: userSchema.parse
};
// ✅ Reuse schemas across related routes
// src/routes/users/[id]/edit/+page.ts
import { userSchema } from '../schemas'; // Shared parent schema
// ✅ Import from client config for browser code
import { urlGenerator } from '$lib/.generated/skroutes-client-config';
// ✅ Import from server config for SSR/API routes
import { urlGenerator } from '$lib/.generated/skroutes-server-config';
// ✅ Use conditional imports for universal code
const config = import.meta.env.SSR
? await import('$lib/.generated/skroutes-server-config')
: await import('$lib/.generated/skroutes-client-config');
// For projects with non-standard directory structure
skRoutesPlugin({
routesDirectory: 'app/routes', // Custom routes location
clientOutputPath: 'src/generated/client-routes.ts',
serverOutputPath: 'src/generated/server-routes.ts'
});
// For monorepos or custom setups
skRoutesPlugin({
routesDirectory: 'apps/web/src/routes', // Nested routes directory
errorURL: '/error'
});
Why customize the routes directory?
- Monorepos: Different apps may have routes in different locations
- Migration: Gradually moving from non-standard directory structures
- Custom setup: Projects with unique architectural requirements
// For projects with custom file naming conventions
skRoutesPlugin({
serverFiles: ['+layout.server.ts', '+page.server.ts', '+endpoint.ts'], // Custom server file patterns
clientFiles: ['+layout.ts', '+page.ts', '+client.ts'], // Custom client file patterns
targetVariable: 'routeSchema', // Custom variable name
errorURL: '/error'
});
// For TypeScript-only projects
skRoutesPlugin({
serverFiles: ['+page.server.ts', '+server.ts'], // Only TypeScript files
clientFiles: ['+page.ts'], // Only TypeScript files
targetVariable: 'validation', // Different variable name
errorURL: '/error'
});
// For JavaScript projects
skRoutesPlugin({
serverFiles: ['+page.server.js', '+server.js'], // Only JavaScript files
clientFiles: ['+page.js'], // Only JavaScript files
targetVariable: '_config', // Different variable name
errorURL: '/error'
});
Why customize file patterns and target variables?
- Custom conventions: Match your team's naming conventions
- Language preference: Use only TypeScript or JavaScript files
- Legacy projects: Integrate with existing variable naming patterns
- Framework variations: Support different SvelteKit setup variations
Example with custom target variable:
// src/routes/users/[id]/+page.ts - Using custom target variable
import { z } from 'zod';
// Using custom target variable name
export const routeSchema = {
paramsValidation: z.object({
id: z.string().uuid()
}).parse,
searchParamsValidation: z.object({
tab: z.enum(['profile', 'settings']).optional()
}).parse
};
Creates a route configuration with type-safe utilities.
Options:
config
: Async function returning object mapping route patterns to validation schemaserrorURL
: URL to redirect to on validation errors
Returns:
urlGenerator
: Function to generate validated URLspageInfo
: Client-side route information utility with optional debounced updatesloadConfig
: Async function that must be called to initialize the configuration before using other functions
Initializes the asynchronous route configuration. Must be called before using urlGenerator
, pageInfo
, or other skRoutes functions.
await loadConfig();
Usage Examples:
// src/hooks.server.ts (recommended)
import { loadConfig } from '$lib/routes';
await loadConfig();
// src/hooks.client.ts (recommended)
import { loadConfig } from '$lib/routes';
await loadConfig();
// With error handling
try {
await loadConfig();
console.log('Routes initialized successfully');
} catch (error) {
console.error('Failed to initialize routes:', error);
}
interface RouteConfig {
[routePattern: string]: {
paramsValidation?: StandardSchemaV1<unknown, unknown>;
searchParamsValidation?: StandardSchemaV1<unknown, unknown>;
};
}
urlGenerator({
address: '/users/[id]', // Route pattern
paramsValue?: { id: 'user123' }, // Route parameters
searchParamsValue?: { tab: 'profile' } // Search parameters
})
Returns:
{
address: string;
url: string;
error: boolean;
params: Record<string, unknown>; // Never undefined
searchParams: Record<string, unknown>; // Never undefined
}
pageInfo(
routeId: '/users/[id]', // Route pattern
pageData: () => ({ params: {...}, url: {...} }), // Function returning SvelteKit page data
config?: { // Optional configuration
updateDelay?: 500, // Throttle delay in milliseconds (default: 0)
onUpdate?: (newUrl: string) => void, // Callback for URL changes
updateAction?: 'goto' | 'nil', // Whether to navigate or just update state
debug?: boolean // Enable debug logging
}
)
Returns:
{
current: {
params: Record<string, unknown>; // Current validated params (reactive)
searchParams: Record<string, unknown>; // Current validated search params (reactive)
},
updateParams: (updates: {
params?: Partial<ParamsType>;
searchParams?: Partial<SearchParamsType>;
}) => UrlGeneratorResult,
updateParamsURLGenerator: (updates) => UrlGeneratorResult, // Generate URL without navigation
resetParams: () => void, // Reset to current URL state
hasChanges: boolean // True if internal state differs from URL
}
Recommended: Migrate from manual configuration to the Vite plugin for better DX and maintainability.
// src/lib/routes.ts - Manual maintenance required ❌
import { skRoutes } from 'skroutes';
import { z } from 'zod';
export const { urlGenerator, pageInfo } = skRoutes({
config: {
// Must manually add each route ❌
'/users/[id]': {
paramsValidation: z.object({ id: z.string().uuid() }).parse,
searchParamsValidation: z.object({
tab: z.enum(['profile', 'settings']).optional()
}).parse
},
'/products/[category]': {
paramsValidation: z.object({
category: z.enum(['electronics', 'clothing', 'books'])
}).parse
}
// Easy to forget routes, get out of sync ❌
},
errorURL: '/error'
});
// vite.config.ts - One-time setup ✅
export default defineConfig({
plugins: [
sveltekit(),
skRoutesPlugin({
errorURL: '/error',
unconfiguredParams: 'never', // Strict validation
unconfiguredSearchParams: 'simple'
})
]
});
// src/routes/users/[id]/+page.ts - Validation alongside route logic ✅
export const _routeConfig = {
paramsValidation: z.object({ id: z.string().uuid() }).parse,
searchParamsValidation: z.object({
tab: z.enum(['profile', 'settings']).optional()
}).parse
};
export async function load({ params }) {
// params.id is already validated as UUID ✅
return { user: await getUser(params.id) };
}
// src/routes/products/[category]/+page.ts - Automatic discovery ✅
export const _routeConfig = {
paramsValidation: z.object({
category: z.enum(['electronics', 'clothing', 'books'])
}).parse
};
// Route automatically discovered and typed ✅
// Usage stays the same ✅
import { urlGenerator } from '$lib/.generated/skroutes-client-config';
const url = urlGenerator({
address: '/users/[id]',
paramsValue: { id: 'user123' }
});
Migration Benefits:
- ✅ Zero maintenance: Routes auto-discovered from file system
- ✅ Co-location: Validation lives with route logic
- ✅ Type safety: Automatic TypeScript integration
- ✅ Hot reload: Instant feedback during development
- ✅ Server/client optimization: Separate configs for different environments
Major Changes:
- Function-based pageInfo:
pageInfo
now takes a function that returns page data for better reactivity - Configuration Object: Parameters are now passed as a configuration object instead of positional arguments
- Bi-directional Sync: Direct binding to
current.params
andcurrent.searchParams
with automatic throttling - New Utilities: Added
resetParams()
,hasChanges
, andupdateParamsURLGenerator()
- Enhanced Debugging: Comprehensive debug logging with the
debug
option
Old Usage:
// Old way with positional arguments
const { current, updateParams } = pageInfo(
routeId,
$page,
500, // delay
goto // onUpdate callback
);
New Usage:
// New way with function and configuration object
const route = pageInfo(routeId, () => $page, {
updateDelay: 500,
onUpdate: goto,
updateAction: 'goto', // or 'nil' for state-only updates
debug: true // Enable debugging
});
// New features available
console.log(route.hasChanges); // Check for unsaved changes
route.resetParams(); // Reset to URL state
// Direct binding now works with throttling
route.current.searchParams = { newValue: 'test' }; // Automatically throttled
Enhanced Reactive Patterns:
// You can now bind directly to route parameters
<input bind:value={route.current.searchParams.query} />
// Or use reactive effects
$effect(() => {
if (someCondition) {
route.current.searchParams.filter = 'active';
}
});
// Check for unsaved changes
{#if route.hasChanges}
<button onclick={route.resetParams}>Reset Changes</button>
{/if}
Contributions are welcome! Please read our contributing guidelines and submit pull requests to our GitHub repository.
MIT License - see LICENSE file for details.