npm package: @pocket.software/address-kit
Description: Typed address and URL registry for framework infrastructure
Latest version: 0.1.0

# @pocket.software/address-kit

Typed address and URL registry for framework infrastructure.

This package is built around one idea:

**addresses should be defined semantically, not assembled ad hoc at call sites.**

That means you define stable infrastructure once:

- `cdn`
- `api`
- `auth`
- `media`
- `localApi`

and then resolve addresses from those named presets with a small, composable API.

---

## Why this pattern is the strongest one

From the patterns you were playing with, the cleanest architecture is:

- **pure functional core** for normalization and rendering
- **typed registry** for named infrastructure targets
- **route-template layer** for ergonomic framework-facing calls

That is much stronger than:

- punctuation-level primitives like `DOT`, `SLASH`, `COLON`
- one giant positional function
- a mutable class as the primary abstraction

The package separates responsibilities cleanly:

- `buildAddress` renders a final address
- `createAddressRegistry` resolves named presets
- `createAddressRoutes` binds route templates onto that registry

---

## Install

```bash
npm install @pocket.software/address-kit
```

---

## Package surface

```ts
import {
  buildAddress,
  buildHost,
  buildQuery,
  buildHash,
  joinPath,
  createAddressRegistry,
  createAddressRoutes,
  defineRoute,
  defineStaticRoute,
} from '@pocket.software/address-kit';
```

---

# 1. Core builder

Use `buildAddress` when you want a small, pure address renderer without a registry yet.

```ts
import { buildAddress } from '@pocket.software/address-kit';

const value = buildAddress(
  {
    protocol: 'https',
    subdomain: 'public',
    domain: 'hostquarter',
    tld: 'com',
    basePath: ['assets'],
  },
  {
    path: ['images', 'logo.png'],
    query: { v: '7' },
    hash: 'preview',
  },
);

console.log(value);
// https://public.hostquarter.com/assets/images/logo.png?v=7#preview
```

### Raw host support

If you need a host like `localhost:3000`, use `host` directly.

```ts
const local = buildAddress(
  {
    protocol: 'http',
    host: 'localhost:3000',
    basePath: ['api'],
  },
  {
    path: ['users'],
  },
);

console.log(local);
// http://localhost:3000/api/users
```

### Domain + tld + port support

```ts
const withPort = buildAddress(
  {
    protocol: 'https',
    subdomain: 'api',
    domain: 'example',
    tld: 'com',
    port: 8443,
    basePath: ['v1'],
  },
  {
    path: ['health'],
  },
);

console.log(withPort);
// https://api.example.com:8443/v1/health
```

---

# 2. Normalization helpers

These are exported because sometimes framework code needs them independently.

```ts
import {
  joinPath,
  buildQuery,
  buildHash,
} from '@pocket.software/address-kit';

console.log(joinPath('/assets/', '/images/', 'logo.png'));
// assets/images/logo.png

console.log(buildQuery({ page: 2, tag: ['audio', 'lecture'] }));
// ?page=2&tag=audio&tag=lecture

console.log(buildHash('preview'));
// #preview
```

---

# 3. Registry-driven addressing

This is the main pattern for a framework.

You define your infrastructure once:

```ts
import { createAddressRegistry } from '@pocket.software/address-kit';

const ADDRESSES = createAddressRegistry({
  cdn: {
    protocol: 'https',
    subdomain: 'public',
    domain: 'hostquarter',
    tld: 'com',
    basePath: ['assets'],
  },

  api: {
    protocol: 'https',
    subdomain: 'api',
    domain: 'hostquarter',
    tld: 'com',
    basePath: ['v1'],
  },

  auth: {
    protocol: 'https',
    subdomain: 'auth',
    domain: 'hostquarter',
    tld: 'com',
  },

  media: {
    protocol: 'https',
    subdomain: 'media',
    domain: 'hostquarter',
    tld: 'com',
    basePath: ['content'],
  },

  localApi: {
    protocol: 'http',
    host: 'localhost:3000',
    basePath: ['api'],
  },
});
```

Then build from intent:

```ts
const logoUrl = ADDRESSES.build('cdn', {
  path: ['images', 'logo.png'],
  query: { v: '42' },
});

const usersUrl = ADDRESSES.build('api', {
  path: ['users'],
  query: { page: 1, limit: 20 },
});

const loginUrl = ADDRESSES.build('auth', {
  path: ['login'],
});

const mediaUrl = ADDRESSES.build('media', {
  path: ['lectures', 'alan-watts.mp3'],
});

const localUsers = ADDRESSES.build('localApi', {
  path: ['users'],
});
```

Outputs:

```ts
// https://public.hostquarter.com/assets/images/logo.png?v=42
// https://api.hostquarter.com/v1/users?page=1&limit=20
// https://auth.hostquarter.com/login
// https://media.hostquarter.com/content/lectures/alan-watts.mp3
// http://localhost:3000/api/users
```

---

# 4. Scoped registry access

If you want a pre-bound helper for a single preset:

```ts
const api = ADDRESSES.scope('api');

const usersUrl = api.build({
  path: ['users'],
  query: { page: 1 },
});
```

That is especially nice when wiring a specific service module.

---

# 5. Extending a registry

You can derive a new registry without mutating the old one.

```ts
const EXTENDED = ADDRESSES.extend('edgeApi', {
  protocol: 'https',
  host: 'edge.example.com',
  basePath: ['v2'],
});

const edgeHealth = EXTENDED.build('edgeApi', {
  path: ['health'],
});
```

---

# 6. Per-call preset overrides

Sometimes you want to keep the semantic preset but swap the actual host or protocol for a dev target.

```ts
const devHealth = ADDRESSES.build('api', {
  path: ['health'],
  overrides: {
    protocol: 'http',
    host: 'localhost:3001',
    basePath: [],
  },
});

console.log(devHealth);
// http://localhost:3001/health
```

This is cleaner than creating one-off string builders all over a codebase.

---

# 7. Route-template layer

This is the nicest framework-facing layer.

You define route shapes once and bind them to the registry.

## Static route

```ts
import {
  createAddressRoutes,
  defineStaticRoute,
} from '@pocket.software/address-kit';

const ROUTES = createAddressRoutes(ADDRESSES, {
  api: {
    users: defineStaticRoute({
      path: ['users'],
    }),
  },
});

console.log(ROUTES.api.users());
// https://api.hostquarter.com/v1/users
```

## Parameterized route

```ts
import {
  createAddressRoutes,
  defineRoute,
} from '@pocket.software/address-kit';

const ROUTES = createAddressRoutes(ADDRESSES, {
  api: {
    userById: defineRoute((userId: string) => ({
      path: ['users', userId],
    })),
  },
});

console.log(ROUTES.api.userById('42'));
// https://api.hostquarter.com/v1/users/42
```

## Object-shaped route params

```ts
const ROUTES = createAddressRoutes(ADDRESSES, {
  api: {
    userSearch: defineRoute(
      (input: { query: string; page?: number; tags?: string[] }) => ({
        path: ['users', 'search'],
        query: {
          q: input.query,
          page: input.page ?? 1,
          tag: input.tags,
        },
      }),
    ),
  },
});

console.log(
  ROUTES.api.userSearch({
    query: 'alan watts',
    page: 2,
    tags: ['audio', 'lecture'],
  }),
);

// https://api.hostquarter.com/v1/users/search?q=alan+watts&page=2&tag=audio&tag=lecture
```

## CDN asset route template

```ts
const ROUTES = createAddressRoutes(ADDRESSES, {
  cdn: {
    asset: defineRoute(
      (input: { folders?: string[]; file: string; version?: string }) => ({
        path: [...(input.folders ?? []), input.file],
        query: input.version ? { v: input.version } : undefined,
      }),
    ),
  },
});

console.log(
  ROUTES.cdn.asset({
    folders: ['images'],
    file: 'logo.png',
    version: '7',
  }),
);

// https://public.hostquarter.com/assets/images/logo.png?v=7
```

---

# 8. Route templates with extra per-call options

A bound route can still accept extra registry-level options.

```ts
const url = ROUTES.api.userById('42', {
  query: { expand: true },
  hash: 'profile',
});

console.log(url);
// https://api.hostquarter.com/v1/users/42?expand=true#profile
```

You can even redirect the same semantic route to a dev host:

```ts
const localUrl = ROUTES.api.userById('42', {
  overrides: {
    protocol: 'http',
    host: 'localhost:3001',
    basePath: [],
  },
});

console.log(localUrl);
// http://localhost:3001/users/42
```

---

# 9. The design philosophy

The package intentionally does **not** make a mutable `class` the main public API.

Why:

- pure functions are easier to test
- immutable preset objects are easier to reason about
- registry-driven semantic naming scales better in a framework
- object-based build calls are easier to extend later

The most important idea here is the move from:

```ts
'https://' + something + '/' + somethingElse
```

to:

```ts
ADDRESSES.build('api', {
  path: ['users', userId],
  query: { expand: true },
});
```

and then eventually:

```ts
ROUTES.api.userById(userId)
```

That is the real abstraction improvement.

---

# 10. Recommended file organization in a larger framework

A good shape is:

```text
address/
  registry.ts
  routes.ts
  helpers.ts
```

Example:

## `address/registry.ts`

```ts
import { createAddressRegistry } from '@pocket.software/address-kit';

export const ADDRESSES = createAddressRegistry({
  cdn: {
    protocol: 'https',
    subdomain: 'public',
    domain: 'hostquarter',
    tld: 'com',
    basePath: ['assets'],
  },

  api: {
    protocol: 'https',
    subdomain: 'api',
    domain: 'hostquarter',
    tld: 'com',
    basePath: ['v1'],
  },

  localApi: {
    protocol: 'http',
    host: 'localhost:3000',
    basePath: ['api'],
  },
});
```

## `address/routes.ts`

```ts
import {
  createAddressRoutes,
  defineRoute,
  defineStaticRoute,
} from '@pocket.software/address-kit';
import { ADDRESSES } from './registry';

export const ROUTES = createAddressRoutes(ADDRESSES, {
  api: {
    users: defineStaticRoute({
      path: ['users'],
    }),

    userById: defineRoute((userId: string) => ({
      path: ['users', userId],
    })),
  },

  cdn: {
    asset: defineRoute(
      (input: { folders?: string[]; file: string; version?: string }) => ({
        path: [...(input.folders ?? []), input.file],
        query: input.version ? { v: input.version } : undefined,
      }),
    ),
  },
});
```

## `address/helpers.ts`

```ts
import { ROUTES } from './routes';

export const assetUrl = (
  folders: string[],
  file: string,
  version?: string,
): string =>
  ROUTES.cdn.asset({
    folders,
    file,
    version,
  });

export const userUrl = (userId: string): string =>
  ROUTES.api.userById(userId);
```

---

# 11. API reference

## `buildAddress(preset, options?)`

Build a URL from a preset and optional `path`, `query`, and `hash`.

## `buildHost(preset)`

Render just the host portion.

## `joinPath(...parts)`

Normalize path segments and join them with one slash.

## `buildQuery(query)`

Render query params into a string.

## `buildHash(hash)`

Render a hash fragment into a string.

## `createAddressRegistry(registry)`

Create a typed registry of named presets.

Returns:

- `registry`
- `get(name)`
- `has(name)`
- `build(name, options?)`
- `scope(name)`
- `extend(name, preset)`

## `defineRoute(fn)`

Define a parameterized route template.

## `defineStaticRoute(options)`

Define a no-arg route template.

## `createAddressRoutes(registry, definitions)`

Bind route templates to a registry and produce typed route functions.

---

# 12. Running locally

```bash
npm install
npm run typecheck
npm run test
npm run build
```

---

# 13. Included files in this zip

- full TypeScript source
- test suite
- examples
- package metadata
- build configs
- README

---

# 14. Final recommendation

Use the package in three layers:

1. **`buildAddress`** for raw composition
2. **`createAddressRegistry`** for semantic infrastructure resolution
3. **`createAddressRoutes`** for ergonomic framework route templates

That is the cleanest long-term pattern from everything you listed.