npm package: @gamesome/route-builder
Description: Typesafe, ergonomic javascript route-builder. No framework dependencies or magic string nonsense.
Author: Johan Svensson
Latest version: 0.1.0

# @gamesome/route-builder

Have you ever been frustrated by not having a good way to manage your application's routes in a type-safe manner? Are you resorting to magic strings scattered throughout your codebase, and constantly breaking prod when your api server tries to redirect to a non-existing route?

@gamesome/route-builder is here to help!

You can now build your application's routes in a type-safe way with support for dynamic segments like so:

```typescript
import { buildRoutes } from '@gamesome/route-builder';

const routes = buildRoutes({
  $: '/',
    user: {
      $: '/users',
      id: (userId: string) => `/${userId}`,
    },
});

routes.$; // "/"
routes.user.$; // "/users"
routes.user.id('123'); // "/users/123"
```

You can also get fancy by creating a branded type like so:

```typescript
type UserId = string & { __brand: 'UserId' };

const routes = buildRoutes({
  user: {
    id: (userId: string) => `/${userId as UserId}`,
  },
});

routes.user.id('123'); // "/users/${UserId}"
```

If you need base urls in your routes you can do that as well. You configure this in the second argument to `buildRoutes` by passing an object with a `baseUrl` property. If you want both relative and absolute urls you can set `inSeparateBranch` to `true` in the same object. Per default the base url will be represented as `BaseUrl` in typehints, but if you want the actual string you can set `fullBaseUrlInTypeHints` to `true`.

```typescript
const routes = buildRoutes(
  {
	$: '/',
	about: {
	  $: '/about',
	},
  },
  {
	baseUrl: 'https://example.com',
	inSeparateBranch: true,
	fullBaseUrlInTypeHints: true,
  }
);

routes.$; // "/"
routes.about.$; // "/about"
routes.withBaseUrl.$; // "https://example.com/"
routes.withBaseUrl.about.$; // "https://example.com/about"
```

## Type hints

In your IDE you will see autocompletion for both static and dynamic routes. as well as hints indicating what will be generated.

### Preview of the entire route tree

<picture>
  <img src="https://github.com/user-attachments/assets/25d20f84-415d-46e5-ac70-66672f2714ca" alt="dynamic route typehint" width="50%" />
</picture>

### Preview of an entry

<picture>
  <img src="https://github.com/user-attachments/assets/fc24d58b-0ed2-435d-8c66-029da1fcd2f9" alt="routes typehint" width="50%" />
</picture>

### Custom base url

<picture>
  <img src="https://github.com/user-attachments/assets/05cca05a-9437-4a46-8fcc-d60e027997d2" alt="routes typehint" width="50%" />
</picture>

### Shortened custom base url

<picture>
  <img src="https://github.com/user-attachments/assets/c6314c17-abe7-4e75-873d-1d88a1628856" alt="routes typehint" width="50%" />
</picture>

## Use cases

- More ergonomic way of routing in your frontend application
- Create a stable contract between your frontend and backend regarding frontend pages and their parameters
- Organise your [ts-rest](https://ts-rest.com) api routes in a type-safe manner (if you for some reason don't use ts-rest yet, you really should check it out! Probably works in other setups as well though)

## Installation

```bash
npm install @gamesome/route-builder
# or
yarn add @gamesome/route-builder
```

## Publishing routes from a package

Most `tsconfig.json` setups work out of the box. If you export routes from a built package with `declaration: true`, TypeScript will emit fully typed `.d.ts` files — no extra tooling required. Just make sure your `package.json` exposes the right entry points (an `exports` map with `types` and `import` conditions, and/or top-level `main` + `types`).

### Exception: `isolatedDeclarations`

When `isolatedDeclarations: true` is enabled, TypeScript requires every export to carry an explicit type annotation that the declaration emitter can resolve **without running the type checker**. Because `buildRoutes` returns a deeply inferred type, `tsc` will error on the bare export.

#### Why not just use `as const` or a helper function?

You might think you can sidestep the generator by extracting the route config into a variable with `as const` and annotating the export with a type like `InferRoutes<typeof config>`. This runs into two problems:

1. **`as const` doesn't narrow function return types.** Arrow functions inside the config (dynamic route segments) still need individual explicit return type annotations, which quickly becomes verbose and error-prone for nested routes.
2. **A helper function like `defineRoutes()` can't be resolved in isolation.** `isolatedDeclarations` requires that types are determinable without cross-file type inference. A function call's return type depends on the function's generic signature in another module, which the declaration emitter can't resolve.

Both approaches break down for any non-trivial route map that includes dynamic segments. The generator exists specifically to solve this — it pre-computes the fully resolved type and writes it to a `.ts` file that the declaration emitter can consume as-is.

For this case the library ships a generator CLI (`route-builder-generate`) and a companion function (`buildRoutesWithGenerator`):

```typescript
import { buildRoutesWithGenerator } from '@gamesome/route-builder/generator';
import type { AppRoutes } from './routes.generated';

export const appRoutes: AppRoutes = buildRoutesWithGenerator({
  $: '/',
  users: {
    $: '/users',
    id: (userId: string) => `/${userId}`,
  },
});
```

Then generate the type file as part of your build:

```bash
route-builder-generate src/index.ts \
  --out src/routes.generated.ts \
  --export appRoutes \
  --type AppRoutes
```

- `src/index.ts` — the source file that contains the route definition.
- `--out` — where to write the generated type file.
- `--export` — the name of the exported variable to read the type from (must match your `export const …`).
- `--type` — the name of the generated type alias (must match the `import type { … }` in your source).

Use a `.ts` extension for `--out` (not `.d.ts`) so that `tsc` emits the corresponding `.d.ts` into `dist/` automatically.