@michael.thms.uk/link-shortener (2.0.1)
Installation
@michael.thms.uk:registry=npm config set @michael.thms.uk:registry=npm install @michael.thms.uk/[email protected]"@michael.thms.uk/link-shortener": "2.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 Node.js with a small adapter.
Install
The package is published to my Forgejo registry. Point the scope at it once, in your project's (or your home directory's) .npmrc:
@michael.thms.uk:registry=https://code.thms.uk/api/packages/michael/npm/
Then install it:
npm install @michael.thms.uk/link-shortener
Usage
Cloudflare Workers
import { createShortener } from "@michael.thms.uk/link-shortener";
export default createShortener({
redirects: {
blog: "https://example.com/blog",
gh: "https://github.com/github",
},
rootRedirect: "https://example.com/",
}) satisfies ExportedHandler<Env>;
Deno
Add the package with deno add npm:@michael.thms.uk/link-shortener (it reads the same .npmrc). Then run with deno run --allow-net main.ts:
import { createShortener } from "@michael.thms.uk/link-shortener";
const shortener = createShortener({
redirects: {
blog: "https://example.com/blog",
gh: "https://github.com/github",
},
rootRedirect: "https://example.com/",
});
Deno.serve((request) => shortener.fetch(request));
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";
import { createShortener } from "@michael.thms.uk/link-shortener";
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
import { createShortener } from "@michael.thms.uk/link-shortener";
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%A9matches acafékey. Paths with malformed percent-encoding return 404. - Slugs are case-sensitive:
fooandFOOare different entries. - Targets must be absolute URLs. Invalid targets (e.g.
/bar) are skipped with aconsole.errorwhen the shortener is created, and requests for them return 404. An invalidrootRedirectis 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/barare looked up as the slugfoo/bar. - All HTTP methods are redirected, not only
GET. - Redirects carry no
Cache-Controlheader. Remember that browsers cache301and308responses aggressively, so prefer302/307while links might still change.
Development
Requires Node.js 26 or newer (see .nvmrc).
npm install
npm run check # typecheck, lint, format 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 |