@pocket.software/address-kit
v0.1.0
Published
Typed address and URL registry for framework infrastructure
Maintainers
Readme
@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:
cdnapiauthmedialocalApi
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:
buildAddressrenders a final addresscreateAddressRegistryresolves named presetscreateAddressRoutesbinds route templates onto that registry
Install
npm install @pocket.software/address-kitPackage surface
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.
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#previewRaw host support
If you need a host like localhost:3000, use host directly.
const local = buildAddress(
{
protocol: 'http',
host: 'localhost:3000',
basePath: ['api'],
},
{
path: ['users'],
},
);
console.log(local);
// http://localhost:3000/api/usersDomain + tld + port support
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/health2. Normalization helpers
These are exported because sometimes framework code needs them independently.
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'));
// #preview3. Registry-driven addressing
This is the main pattern for a framework.
You define your infrastructure once:
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:
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:
// 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/users4. Scoped registry access
If you want a pre-bound helper for a single preset:
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.
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.
const devHealth = ADDRESSES.build('api', {
path: ['health'],
overrides: {
protocol: 'http',
host: 'localhost:3001',
basePath: [],
},
});
console.log(devHealth);
// http://localhost:3001/healthThis 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
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/usersParameterized route
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/42Object-shaped route params
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=lectureCDN asset route template
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=78. Route templates with extra per-call options
A bound route can still accept extra registry-level options.
const url = ROUTES.api.userById('42', {
query: { expand: true },
hash: 'profile',
});
console.log(url);
// https://api.hostquarter.com/v1/users/42?expand=true#profileYou can even redirect the same semantic route to a dev host:
const localUrl = ROUTES.api.userById('42', {
overrides: {
protocol: 'http',
host: 'localhost:3001',
basePath: [],
},
});
console.log(localUrl);
// http://localhost:3001/users/429. 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:
'https://' + something + '/' + somethingElseto:
ADDRESSES.build('api', {
path: ['users', userId],
query: { expand: true },
});and then eventually:
ROUTES.api.userById(userId)That is the real abstraction improvement.
10. Recommended file organization in a larger framework
A good shape is:
address/
registry.ts
routes.ts
helpers.tsExample:
address/registry.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
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
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:
registryget(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
npm install
npm run typecheck
npm run test
npm run build13. 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:
buildAddressfor raw compositioncreateAddressRegistryfor semantic infrastructure resolutioncreateAddressRoutesfor ergonomic framework route templates
That is the cleanest long-term pattern from everything you listed.
