@sirmekus/uzor
v1.0.0
Published
Lightweight, type-safe route generator. Define routes in a config file and resolve them anywhere - with param interpolation, query-string support, and optional absolute URLs.
Downloads
103
Maintainers
Readme
@sirmekus/uzor
Lightweight, type-safe route generator for TypeScript and JavaScript projects.
Define your routes once in a config file. Resolve them anywhere — with {param} interpolation, automatic query-string handling, and optional absolute URLs.
Igbo word of the day: Uzor ,in Igbo language, means "road" 😉
Why uzor?
In most frontend projects, URLs are written as raw strings scattered across components, hooks, and services:
fetch(`/api/users/${userId}/posts?page=${page}`)
axios.get(`/api/posts/${postId}/comments/${commentId}`)
router.push(`/dashboard/${orgId}/settings`)This pattern silently accumulates a class of bugs that are easy to introduce and hard to track down:
- Typos are invisible.
/api/usres/42compiles and ships without complaint. - Refactoring is risky. Renaming or restructuring an endpoint means hunting through the entire codebase with find-and-replace, hoping nothing is missed.
- Inconsistency creeps in. One call encodes params, another doesn't. One appends a trailing slash, another doesn't. Edge cases in query-string serialisation (
null,undefined, special characters) are handled differently in every file. - There is no single source of truth. Routes live wherever they happen to be used, making it impossible to audit what endpoints a project actually calls.
uzor solves all of this by moving routes to a single config file and providing one function — route() — to resolve them. The rest of the codebase never touches a URL string directly.
What you gain
A single source of truth. Every route the project uses is declared in one place. Adding, removing, or renaming an endpoint is a one-line change, and every call site updates automatically.
Compile-time safety.
Route names are typed as keyof of your config. Passing a name that doesn't exist is a TypeScript error before the code ever runs. No more silent 404s caused by a mistyped route name.
Consistent URL construction — always.
Placeholders are URI-encoded. Unused params become a properly formatted query string. Trailing slashes are normalised. null and undefined values are dropped cleanly. You write none of this logic yourself, and it behaves the same way everywhere.
Autocomplete on route names.
Because defineRoutes() preserves the literal type of your config, editors show a dropdown of valid route names as you type the first argument to route(). Discovering available routes becomes as easy as pressing Ctrl+Space.
Framework and environment agnostic. uzor has zero dependencies and no opinions about your stack. It works in the browser, in Node.js, in React, Vue, Svelte, or plain TypeScript. The router is a plain object, not a hook or a context — create it once, import it anywhere.
Installation
npm install @sirmekus/uzorNo peer dependencies. Works in any environment: browser, Node.js, React, Vue, plain TS.
Quick start
1. Define your routes
Create a config file — the conventional name is routes.config.ts, but any name works.
// routes.config.ts
import { defineRoutes } from '@sirmekus/uzor';
export default defineRoutes({
'home': '/',
'dashboard': '/dashboard',
'api.users': '/api/users/{id}',
'api.posts': '/api/posts/{postId}/comments/{commentId}',
'api.search': '/api/search',
'auth.login': 'https://auth.example.com/v1/login',
});2. Create a router and export route
Call createRouter once in a dedicated file and export the route function. Import it anywhere you need to resolve a URL.
// router.ts
import { createRouter } from '@sirmekus/uzor';
import routes from './routes.config';
export const { route } = createRouter(routes);3. Import and use route wherever you need it
// any file in your project
import { route } from './router';
route('home') // '/'
route('api.users', { id: 42 }) // '/api/users/42'
route('api.posts', { postId: 5, commentId: 3 }) // '/api/posts/5/comments/3'
route('api.search', { q: 'hello world', page: 2 }) // '/api/search?q=hello%20world&page=2'
route('home', {}, true) // 'https://example.com'API
defineRoutes(config)
An identity helper used when declaring your route config. It returns the config object unchanged but preserves the full TypeScript literal type, which enables autocomplete on route names throughout your app.
import { defineRoutes } from '@sirmekus/uzor';
export default defineRoutes({
'api.users': '/api/users/{id}',
'api.posts': '/api/posts/{postId}',
});Without
defineRoutes, TypeScript infersRecord<string, string>and you lose name autocomplete.
createRouter(routes)
Takes your route config and returns a { route, routes } object.
import { createRouter } from '@sirmekus/uzor';
import routes from './routes.config';
const { route, routes: allRoutes } = createRouter(routes);| Property | Type | Description |
|----------|------|-------------|
| route | RouteFn<T> | Resolves a named route to a URL string. |
| routes | T | The original config passed in. |
route(name, params?, absolute?)
Resolves a named route to a URL string.
| Argument | Type | Default | Description |
|------------|----------------|---------|-------------|
| name | keyof T | — | A key from your route config. TypeScript will error on unknown names. |
| params | RouteParams | {} | Values to interpolate into {placeholders} and/or append as query-string entries. |
| absolute | boolean | false | When true, prepends window.location.protocol + '//' + window.location.host. Browser-only — throws in Node.js/SSR. |
Throws Error if name is not in the config.
How URL resolution works
Given the template /api/posts/{postId}/comments/{commentId} and params:
Step 1 — Interpolate placeholders
{postId} → encodeURIComponent(params.postId)
{commentId} → encodeURIComponent(params.commentId)
Step 2 — Remove unmatched placeholders
Any {placeholder} with no matching param key is stripped.
Step 3 — Strip trailing slash
'/users/' → '/users' (bare '/' is kept as-is)
Step 4 — Append remaining params as query string
Keys not consumed by a placeholder → '?key=value&key2=value2'
If the template already contains '?', uses '&' instead.
Step 5 — Make absolute (optional)
Prepends window.location.protocol + '//' + window.location.hostundefined, null, and '' param values are silently ignored — they neither fill a placeholder nor appear in the query string.
Examples
Path interpolation
route('api.users', { id: 99 })
// '/api/users/99'Query string (unused params)
route('api.search', { q: 'cats', page: 2, limit: 10 })
// '/api/search?q=cats&page=2&limit=10'Mixed — some params fill placeholders, rest become query string
// template: '/api/posts/{postId}'
route('api.posts', { postId: 5, sort: 'desc', page: 1 })
// '/api/posts/5?sort=desc&page=1'Optional params — omit by passing undefined or null
// template: '/api/users/{id}/avatar'
route('api.users.avatar', { id: undefined })
// '/api/users/avatar' — {id} is stripped, trailing slash removedAbsolute URL (browser only)
// window.location = 'https://app.example.com/...'
route('dashboard', {}, true)
// 'https://app.example.com/dashboard'External / absolute template
Templates can be full URLs too:
defineRoutes({
'auth.login': 'https://auth.example.com/v1/login',
})
route('auth.login', { redirect: '/dashboard' })
// 'https://auth.example.com/v1/login?redirect=%2Fdashboard'TypeScript
Route names are fully type-checked. Passing an unknown name is a compile-time error:
const { route } = createRouter(defineRoutes({
'home': '/',
'api.users': '/api/users/{id}',
}));
route('home') // OK
route('api.users', { id: 1 }) // OK
route('bad.name') // TS Error: Argument of type '"bad.name"' is not assignableThe RouteParams type accepts string | number | boolean | null | undefined values, so you can pass numbers directly without converting:
route('api.users', { id: 42, page: 3, active: true })Organising large configs
Split routes by domain and merge them before passing to createRouter:
// routes/api.ts
import { defineRoutes } from '@sirmekus/uzor';
export const apiRoutes = defineRoutes({
'api.users': '/api/users/{id}',
'api.posts': '/api/posts/{postId}',
});
// routes/auth.ts
import { defineRoutes } from '@sirmekus/uzor';
export const authRoutes = defineRoutes({
'auth.login': 'https://auth.example.com/v1/login',
'auth.logout': 'https://auth.example.com/logout',
});
// routes/index.ts
import { createRouter } from '@sirmekus/uzor';
import { apiRoutes } from './api';
import { authRoutes } from './auth';
export const { route } = createRouter({ ...apiRoutes, ...authRoutes });License
MIT — sirmekus (aka Emmy Boy)
