Skip to content

Lightweight TypeScript library to efficiently maintain and build URLs

Notifications You must be signed in to change notification settings

BearStudio/lunalink

Repository files navigation

lunalink

luna the cat with the title lunalink on her head

This library is an alternative to urlcat which isn't maintained anymore. That is why it is named after my cat, Luna πŸˆβ€β¬›. Link is because it is easy to say after luna. That's it, that's the whole story.

I (@yoannfleurydev), did not want to fork urlcat to challenge myself into reimplementing it. Some API design are made to simplify my development, I hope it will simplify yours too.

Lunalink main purpose is to provide utilities to, given a url, extract parameters and replace them with real values. No more Record<string, string> for your url params!

Features

  • πŸ”¬ URL Path params type extraction
  • πŸ” Replace path params placeholders with real values
  • 🀏 Tiny (2.8 kB minified + gzipped)
  • 🟦 TypeScript first
  • πŸ§ͺ Fully tested

Installation

Install the package using your favorite package manager:

npm install @bearstudio/lunalink
yarn add @bearstudio/lunalink
pnpm add @bearstudio/lunalink

Usage

Note

These samples of code were purely invented for the example.

Go from this kind of code:

import { useQuery } from '@tanstack/react-query';
import { pick } from 'remeda';

// πŸ™ You need to maintain path params type as they change in the url, 
// no automatic type updates based on the url
type EventCategoryType = {
  year: string;
  typeId: string;
  categoryId: string;
  filter?: string;
  page?: string;
  size?: string;
}

const useEventCategory = (params: EventCategoryType) => {
  return useQuery({
    queryKey: ['event', 'type', 'category', params],
    queryFn: async () => {
      // πŸ™ Here you need to handle searchParams separately
      const searchParams = new URLSearchParams(pick(params, ['filter', 'page', 'size']));

      // 😩 Tedious url build, hard to read if there is more than two path params
      const response = await fetch(`demo/event/${params.year}/type/${params.typeId}/category/${params.categoryId}?${searchParams.toString()}`);

      return response.json();
    }
  })
}

to this kind of code:

import { useQuery } from '@tanstack/react-query';
import { type ExtractParams, lunalink } from '@bearstudio/lunalink';

const eventCategoryIdRoute = 'demo/event/:year/type/:typeId/category/:categoryId';

// πŸ‘ Let lunalink detect path params given your url, and add your custom searchParams
// The type will always be up to date with path params declared in your url
type EventCategoryIdRouteType = ExtractParams<typeof eventCategoryIdRoute> & {
  filter?: string;
  page?: string;
  size?: string;
};

const useEventCategory = (params: EventCategoryIdRouteType) => {
  return useQuery({
    queryKey: ['event', 'type', 'category', params],
    queryFn: async () => {
      // πŸ‘ Lunalink will replace path params with their values from the params object, 
      // based on how they are defined in eventCategoryIdRoute. 
      // And leftovers will be passed as search params.
      // No more custom url concatenation!
      const response = await fetch(lunalink(eventCategoryIdRoute, params));
      return response.json();
    },
  });
};

useEventCategory({
  year: '2025',
  typeId: 'event',
  categoryId: 'track',
  page: '3',
  size: '10',
});
// Called url will look like this: 'demo/event/2025/type/event/category/track?page=3&size=10'

No more types to maintain, only urls to declare! Easier to read, easier to use πŸ‘Œ

API

lunalink(pathToReplace, pathParams [, options]);

import { lunalink } from '@bearstudio/lunalink';

// lunalink(path, variables, config);

// returns 'a/path/with/a-variable'
lunalink('a/path/with/:variables', { variables: 'a-variable' });

// returns 'a/path/with/a-variable?anotherVar=3'
lunalink('a/path/with/:variables', { variables: 'a-variable', anotherVar: '3' });

// returns 'a/path/my-file.pdf'
lunalink('a/path/:fileName.:ext', { fileName: 'my-file', ext: 'pdf' });

// returns 'https://my.api.com/a/path/with/a-variable'
lunalink(
  'a/path/with/:variables',
  { variables: 'a-variable' },
  { baseURL: 'https://my.api.com' }, // trailing slash is not mandatory
);

// You can override the default encodeURIComponent() method used to encode
// path params
// returns 'a/path/with/A-VARIABLE'
lunalink(
  'a/path/with/:variables',
  { variables: 'a-variable' },
  { 
    encodeURIComponent: (pathParamValue) => {
      return pathParamValue.toLocalUpperCase();
    },
  },
);

ExtractParams<Path extends string>;

This type utility allows you to extract all path params from a url into a Record like object.

Usefull to type function params.

import { ExtractParams } from '@bearstudio/lunalink';

type MyURLPathParams = ExtractParams<'a/path/with/:variable'>;
// type MyURLPathParams = {
//   variable: string;
// };

const url = 'a/path/:fileName.:ext'
type AnotherURLPathParams = ExtractParams<typeof url>;
// type AnotherURLPathParams = {
//   fileName: string;
//   ext: string;
// };

join(path1, path2 [, separator]);

join is a utility fonction which allows to join to path. If join detect a baseURL either in left or right, it will always prepend it to the other param.

If not, the two path will be joined.

import { join } from '@bearstudio/lunalink';

What's inside?

This monorepo includes the following packages/apps:

Apps and Packages

  • web: a Next.js app
  • @repo/lunalink: the source code of the library. Learn mode about it in its dedicated README
  • @repo/eslint-config: eslint configurations (includes eslint-config-next and eslint-config-prettier)
  • @repo/typescript-config: tsconfig.jsons used throughout the monorepo

Each package/app is 100% TypeScript.

Build

To build all apps and packages, run the following command:

pnpm build

Develop

To develop all apps and packages, run the following command:

pnpm dev

Useful Links

Learn more about the power of Turborepo:

About

Lightweight TypeScript library to efficiently maintain and build URLs

Topics

Resources

Code of conduct

Contributing

Stars

Watchers

Forks

Sponsor this project

  •  

Packages

No packages published

Contributors 3

  •  
  •  
  •