@michael.thms.uk/link-shortener (1.0.1)

Published 2026-10-09 08:40:24 +00:00 by michael

Installation

@michael.thms.uk:registry=
npm config set @michael.thms.uk:registry=
npm install @michael.thms.uk/[email protected]
"@michael.thms.uk/link-shortener": "1.0.1"

About this package

link-shortener

A tiny, dependency-free redirect handler built on the standard Request / Response APIs. You give it a map of slugs to URLs and it gives you back an object with a fetch(request) method, so it runs anywhere that speaks the Fetch API: Cloudflare Workers, Deno, Bun, and similar runtimes.

Install

Install straight from the Git repository, pinned to a tag:

npm install git+https://code.thms.uk/michael/link-shortener.git#<version>

The package ships TypeScript source, so your bundler or runtime needs to handle .ts files (Wrangler, Deno and Bun do this out of the box). Node.js won't strip types from files inside node_modules, so there you'll need a bundler.

With Deno, import the source file directly from the repository instead:

import { createShortener } from "https://code.thms.uk/michael/link-shortener/raw/tag/<version>/src/index.ts";

Usage

Cloudflare Workers

import { createShortener } from "link-shortener";

export default createShortener({
  redirects: {
    blog: "https://example.com/blog",
    gh: "https://github.com/github",
  },
  rootRedirect: "https://example.com/",
}) satisfies ExportedHandler<Env>;

Deno

Run with deno serve main.ts:

export default createShortener({
  redirects: {
    blog: "https://example.com/blog",
    gh: "https://github.com/github",
  },
  rootRedirect: "https://example.com/",
}) satisfies Deno.ServeDefaultExport;

To use Deno.serve() instead, pass it the fetch method: Deno.serve(shortener.fetch).

Node.js

Node's built-in node:http server doesn't take a fetch handler, so convert between its request/response objects and the Fetch API:

import { createServer } from "node:http";

const shortener = createShortener({
  redirects: {
    blog: "https://example.com/blog",
    gh: "https://github.com/github",
  },
  rootRedirect: "https://example.com/",
});

createServer(async (req, res) => {
  const url = new URL(
    req.url ?? "/",
    `http://${req.headers.host ?? "localhost"}`,
  );
  const response = await shortener.fetch(
    new Request(url, { method: req.method }),
  );
  res.writeHead(response.status, Object.fromEntries(response.headers));
  res.end(await response.text());
}).listen(3000);

The shortener only looks at the URL and method, so the adapter doesn't forward request headers or bodies.

Bun

Bun.serve(
  createShortener({
    redirects: {
      blog: "https://example.com/blog",
      gh: "https://github.com/github",
    },
    rootRedirect: "https://example.com/",
  }),
);

Options

Option Type Default Description
redirects Record<string, string> (required) Map of slug → absolute target URL.
rootRedirect string none: / is a 404 Where requests to / are sent.
status 301 | 302 | 307 | 308 302 HTTP status used for redirects.
acceptTrailingSlash boolean false Treat /slug/ the same as /slug.
stripQueryParams boolean false Drop the incoming query string instead of passing it on to the target.
notFoundPage string built-in page HTML body returned (with status 404) for unknown slugs.

Behaviour

  • Slugs are URL-decoded before lookup, so /caf%C3%A9 matches a café key. Paths with malformed percent-encoding return 404.
  • Slugs are case-sensitive: foo and FOO are different entries.
  • Targets must be absolute URLs. Invalid targets (e.g. /bar) are skipped with a console.error when the shortener is created, and requests for them return 404. An invalid rootRedirect is treated as unset.
  • Query strings are passed through to the target by default. Incoming parameters override parameters of the same name already on the target URL; if a key is repeated in the request, the last value wins.
  • Only exact matches redirect. Nested paths such as /foo/bar are looked up as the slug foo/bar.
  • All HTTP methods are redirected, not only GET.
  • Redirects carry no Cache-Control header. Remember that browsers cache 301 and 308 responses aggressively, so prefer 302 / 307 while links might still change.

Development

Requires Node.js 26 or newer (see .nvmrc).

npm install
npm run check   # typecheck, lint, format check and tests

Individual steps are available as npm run typecheck, npm run lint, npm run format and npm test.

Dependencies

Development dependencies

ID Version
@types/node ^26.6.4
oxlint ^1.87.0
oxlint-tsgolint ^7.0.2003
prettier ^3.9.9
typescript ^7.0.2
vitest ~5.0.3
Details
npm
2026-10-09 08:40:24 +00:00
8
2026-10-27 22:43:20 +00:00
MIT
5.8 KiB
Assets (1)
Versions (3) View all
2.0.1 2026-10-26
2.0.0 2026-10-09
1.0.1 2026-10-09