- TypeScript 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
All checks were successful
Test / test (push) Successful in 15s
Reviewed-on: #3 |
||
| .forgejo/workflows | ||
| src | ||
| test | ||
| .gitignore | ||
| .nvmrc | ||
| .oxlintrc.json | ||
| .prettierrc | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| renovate.json | ||
| tsconfig.build.json | ||
| tsconfig.json | ||
| vitest.config.ts | ||
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.