@pomle/paths

A JavaScript library for handling web app paths

Usage no npm install needed!

<script type="module">
  import pomlePaths from 'https://cdn.skypack.dev/@pomle/paths';
</script>

README

Paths

A JavaScript and TypeScript lib for handling type rich URLs in web apps.

Usage

Paths

Define paths

import { createPath, codecs } from '@pomle/paths';

const user = createPath('/user/:userId', {
  userId: codecs.string,
});

const userPost = user.append('/posts/:postId', {
  postId: codecs.number,
});

export const paths = {
  user,
  userPost,
};

Create URLs

import { paths } from './paths';

const url = paths.userPost.build({
  userId: 'pomle',
  postId: 24,
});

console.log(url); // Prints "/user/pomle/posts/24"

Parse path params from URLs

import { paths } from './paths';

const values = paths.userPost.parse('/user/pomle/posts/24');

console.log(values); // Prints {userId: "pomle", postId: 24}

Decode params already parsed

import { paths } from './paths';

const values = paths.userPost.decode({
  userId: 'pomle',
  postId: '24',
});

console.log(values); // Prints {userId: "pomle", postId: 24}

Query

Define query params

import { createQuery, codecs } from '@pomle/paths';

const searchFilter = createQuery({
  username: codecs.string,
  range: codecs.number,
});

Parse query string from URL

// Location ?username=Pontus+Alexander&range=3&range=5

const values = searchFilter.parse(window.location.search);

console.log(values);
/* 
{
  username: ["Pontus Alexander"],
  range: [3, 5]
}
*/

Update query string

// Location ?username=Pontus+Alexander&range=3&range=5

const qs = searchFilter.build({
  username: ['Pontus Alexander'],
  range: [3, 5],
});

console.log(qs);
// ?username=Pontus+Alexander&range=3&range=5

Codecs

There are 3 codecs bundled

import { createQuery, codecs } from '@pomle/paths';

codecs.boolean; // Encodes true and false as 1 and 0
codecs.string; // Encodes strings safely for URLs
codecs.number; // Encodes numbers safely for URLs

Create custom codecs. Do not encode and decode with encodeURIComponent or decodeURIComponent manually. It will be handled by the library.

import { createPath, createCodec } from '@pomle/paths';
import { DateTime } from 'luxon';

const dateCodec = createCodec<DateTime>(
  (date) => {
    return date.toISO();
  },
  (text: string) => {
    return DateTime.fromISO(text);
  },
);

const bookings = createPath('/bookings/:dayOfArrival', {
  dayOfArrival: dateCodec,
});

const url = bookings.url({
  dayOfArrival: DateTime.now(),
});

Codec Tips!

Create a one-of-guard to ensure a value is in a set of acceptable values.

function oneOf<T extends unknown>(values: T[]) {
  return function ensureOneOf(value: unknown): T {
    if (!values.includes(value as T)) {
      throw new Error(`Value not one of ${values.join(', ')}`);
    }
    return value as T;
  };
}

Mapping string union to params via codec.

import { createCodec } from '@pomle/paths';
import { oneOf } from "./one-of";

type DoorState = 'open' | 'closed';
const DOOR_STATES: DoorState[] = ['open', 'closed'];

// String unions and URL values are always strings - no conversion of value needed.
const doorCodec = createCodec<DoorState>(
  (source: DoorState) => source.toString(),
  oneOf(DOOR_STATES),
);

Mapping string union or enums to params via codec.

import { createCodec } from '@pomle/paths';
import { oneOf } from "./one-of";

enum MessageStatus {
  Sent,
  Received,
  Read,
}

const MESSAGE_STATUSES = [
  MessageStatus.Sent,
  MessageStatus.Received,
  MessageStatus.Read,
];

const ensureMessageStatus = oneOf(MESSAGE_STATUSES);

// Enums are numbers by default - convert URL value to number and then check.
const messageStatusCodec = createCodec<MessageStatus>(
  (source: MessageStatus) => source.toString(),
  (source: string) => {
    const number = parseFloat(source);
    return ensureMessageStatus(number);
  },
);