1 The Problem
We want a URL shortener: give it a long link, it returns a short code; give back the code, it returns the original link. It teaches two-way lookups (code↔URL), generating unique keys, and persisting a small store — the core of any link service.
2 How to Think About It
Think about the mapping, before any code:
3 The Build — explained part by part
Here is the complete shortener. Read each part’s note below — you should understand the whole thing from the notes alone.
import * as fs from "node:fs";
import { randomInt } from "node:crypto";
const FILE = "links.json";
const CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
export type Links = Record<string, string>;
export function load(): Links {
if (fs.existsSync(FILE)) {
return JSON.parse(fs.readFileSync(FILE, "utf-8")) as Links;
}
return {};
}
export function save(links: Links): void {
fs.writeFileSync(FILE, JSON.stringify(links));
}
// makeCode builds a random short code from letters and digits.
export function makeCode(length = 6): string {
let code = "";
for (let i = 0; i < length; i++) {
code += CHARS[randomInt(0, CHARS.length)];
}
return code;
}
export function shorten(links: Links, url: string): string {
let code = makeCode();
while (code in links) {
// avoid a clash with an existing code
code = makeCode();
}
links[code] = url;
return code;
}
export function expand(links: Links, code: string): string | undefined {
return links[code]; // undefined if the code is unknown
}
function main(): void {
const links = load();
const code = shorten(links, "https://example.com/a/very/long/link");
save(links);
console.log(`Short code: ${code}`);
console.log(`Expands to: ${expand(links, code)}`);
}
if (require.main === module) {
main();
}Record utility type standing in for Python’s plain dict.function makeCode(length = 6): string — a default parameter, just like Python’s
length=6; callers can omit it entirely or override it.while (code in links) — the
in operator checks whether a key exists in an object, used here to avoid a clash with an existing short code, identical in spirit to the Python version’s while code in links.export function expand(links, code): string | undefined — looking up a missing key on a plain object returns
undefined in JavaScript (Python’s dict.get returns None for the same reason); the return type says so explicitly, so callers cannot forget to check.
expand as plain string.string | undefined — the compiler then forces every caller to handle the “not found” case instead of letting it slip through silently.while (code in links) generating a fresh one each time a clash is found — checking before generating would check nothing.Record<string, string> and then also allowing undefined values to sneak in some other way.string; let only the lookup result be optional, which is what expand’s return type already captures.4 Test & Prove Each Part
How do we know this works? We pull the real logic into small, plain functions and check each one against cases we already know the answer to.
undefinedimport { test } from "node:test";
import assert from "node:assert/strict";
import { shorten, expand, makeCode, type Links } from "./url-shortener";
test("shorten then expand returns the original URL", () => {
const links: Links = {};
const code = shorten(links, "https://example.com");
assert.equal(expand(links, code), "https://example.com");
});
test("codes are 6 characters by default", () => {
assert.equal(makeCode().length, 6);
});
test("an unknown code returns undefined", () => {
assert.equal(expand({}, "missing"), undefined);
});Compile with npx tsc then run node --test shortener.test.js. Passing a fresh Links object into shorten/expand keeps each test isolated from links.json on disk.
5 The Interface
What it expects
shorten(links, "https://example.com/very/long")What it returns
Short code: T0Gl9y
Expands to: https://example.com/very/long6 Run It & Automate It
Save the code as shortener.ts, compile with npx tsc, and run with node shortener.js — or run it directly with npx tsx shortener.ts.
npx tsc shortener.ts && node shortener.jsShortens one example URL and immediately expands it back, so you can see both directions work.
A CI tool like Jenkins runs the type-checker and tests automatically whenever the code changes — every line below has a plain explanation.
Short code: 6FVHZg
Expands to: https://example.com/a/very/long/linkmakeCode is really drawing from crypto.randomInt each time, and that links.json is not being reset between runs in a way that hides a stuck generator.expand returns undefined right after shorten just created the codelinks object you just wrote it into — a common mistake is loading a second, stale copy from disk.// Jenkinsfile — runs the type-checker and tests automatically every time the code changes.
pipeline {
agent any // run on any available machine
stages {
stage('Get the code') {
steps { checkout scm } // download the latest code
}
stage('Set up Node') {
steps {
sh 'node --version' // confirm Node is installed
sh 'npm install -D typescript @types/node' // zero runtime deps — just the compiler and its Node types
}
}
stage('Type-check and test') {
steps {
sh 'npx tsc --noEmit' // catch type errors before anything runs
sh 'npx tsc' // compile to plain JavaScript
sh 'node --test shortener.test.js' // Node's built-in test runner, no extra install needed
}
}
}
post {
success { echo 'All tests passed.' }
failure { echo 'A test failed — look above.' }
}
}
You have a working url shortener. Extend it:
- Add an expiry. Store a timestamp alongside each URL and reject expired codes. (Teaches: storing a richer value than a plain string, so
LinksbecomesRecord<string, { url: string; expires: number }>.) - Track click counts. Increment a counter in
expandevery time a code is looked up. (Teaches: updating a nested object field.) - Validate the input URL. Reject anything that is not a well-formed
http(s)://URL before shortening it. (Teaches: the built-inURLclass for parsing and validation.) - Let the caller pick a custom code. Accept an optional preferred code and fall back to a random one only if it is taken. (Teaches: optional parameters.)
Record<K, V> utility type for a string-keyed map, why a lookup that can fail should return T | undefined rather than pretending it cannot fail, and the same secure-random-code pattern used by the password generator. Related reference: Utility Types, Interfaces vs. Types.