npm package: @sirmekus/uzor
Description: 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.
Author: sirmekus
Latest version: 1.0.0

# @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:

```ts
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/42` compiles 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

```bash
npm install @sirmekus/uzor
```

No 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.

```ts
// 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.

```ts
// 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**

```ts
// 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.

```ts
import { defineRoutes } from '@sirmekus/uzor';

export default defineRoutes({
  'api.users': '/api/users/{id}',
  'api.posts': '/api/posts/{postId}',
});
```

> Without `defineRoutes`, TypeScript infers `Record<string, string>` and you lose name autocomplete.

---

### `createRouter(routes)`

Takes your route config and returns a `{ route, routes }` object.

```ts
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.host
```

`undefined`, `null`, and `''` param values are silently ignored — they neither fill a placeholder nor appear in the query string.

---

## Examples

### Path interpolation

```ts
route('api.users', { id: 99 })
// '/api/users/99'
```

### Query string (unused params)

```ts
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

```ts
// 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`

```ts
// template: '/api/users/{id}/avatar'
route('api.users.avatar', { id: undefined })
// '/api/users/avatar'  — {id} is stripped, trailing slash removed
```

### Absolute URL (browser only)

```ts
// window.location = 'https://app.example.com/...'
route('dashboard', {}, true)
// 'https://app.example.com/dashboard'
```

### External / absolute template

Templates can be full URLs too:

```ts
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:

```ts
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 assignable
```

The `RouteParams` type accepts `string | number | boolean | null | undefined` values, so you can pass numbers directly without converting:

```ts
route('api.users', { id: 42, page: 3, active: true })
```

---

## Organising large configs

Split routes by domain and merge them before passing to `createRouter`:

```ts
// 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)](https://github.com/sirmekus)