@michael.thms.uk/link-shortener (1.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": "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%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 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 |