← thecodex.expert · The Codex Family of Knowledge
Tier 2 · Intermediate · TypeScript Project

URL Shortener

Turn long URLs into short codes and back again, saved to a file. Learn two-way lookups and generating unique keys.

🧠 Teaches how to think spoonfed, every age Last verified:

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.

Where this shows up: bit.ly and every link shortener, QR-code targets, affiliate links, any system that maps a short key to a longer value — which includes caches, session stores, and lookup services generally.

2 How to Think About It

Think about the mapping, before any code:

The plan — in plain English
1. Keep a store mapping short code → long URL. → 2. To shorten: make a new random short code, save the mapping, return the code. → 3. To expand: look the code up and return the URL. → 4. Save the store to a file so links survive.

Long URL comes in

Generate a short code

Save code to URL mapping

Return the short code

Short code comes in

Look up the URL

Return the long URL

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.

TypeScriptshortener.ts
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();
}
⚠ No in-browser playground here
Running real, type-checked TypeScript in the browser needs either a full copy of the compiler or a third-party CDN script — the same kind of external dependency this site avoids relying on for a core teaching example. Copy the code below and run it with Node on your own machine instead; the “Run It” section explains exactly how.
What each part does — in plain words
export type Links = Record<string, string> — a type alias for “an object whose keys and values are both strings”, TypeScript’s built-in 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.
Common mistakes — and how to avoid them
✗ Writing the return type of expand as plain string.
✓ A missing code really can happen, so the honest type is string | undefined — the compiler then forces every caller to handle the “not found” case instead of letting it slip through silently.
✗ Generating the code after checking whether it clashes.
✓ Generate first, then loop while (code in links) generating a fresh one each time a clash is found — checking before generating would check nothing.
✗ Using Record<string, string> and then also allowing undefined values to sneak in some other way.
✓ Keep the map’s own values strictly 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.

Shorten then expand returns the original URL
Codes are 6 characters by default
An unknown code returns undefined
TypeScriptshortener.test.ts
import { 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

INPUTSHORTENlong url
What it expects
shorten(links, "https://example.com/very/long")
OUTPUTEXPANDshort code
What it returns
Short code: T0Gl9y
Expands to: https://example.com/very/long

6 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.

Run it locally
npx tsc shortener.ts && node shortener.js
Shortens 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.

What you should see when it works
Terminala real run
Short code: 6FVHZg
Expands to: https://example.com/a/very/long/link
If it breaks — how to fix it
🚨 The same short code is generated every run
Check that makeCode 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 code
Make sure you look the code up in the same links object you just wrote it into — a common mistake is loading a second, stale copy from disk.
GroovyJenkinsfile
// Jenkinsfile &mdash; 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 &mdash; 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 &mdash; look above.' }
    }
}
🎯 Try this next — make it yours

You have a working url shortener. Extend it:

  1. Add an expiry. Store a timestamp alongside each URL and reject expired codes. (Teaches: storing a richer value than a plain string, so Links becomes Record<string, { url: string; expires: number }>.)
  2. Track click counts. Increment a counter in expand every time a code is looked up. (Teaches: updating a nested object field.)
  3. Validate the input URL. Reject anything that is not a well-formed http(s):// URL before shortening it. (Teaches: the built-in URL class for parsing and validation.)
  4. 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.)
What you learned
You learned TypeScript’s 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.