A URL Shortener

devdotdev.dev June 8, 2026
Source

A simple URL shortener that maps long URLs to short codes. Supports creating, retrieving, and expanding shortened URLs. import hashlib import string from abc import ABC, abstractmethod from typing import Dict, Optional from dataclasses import dataclass # Constants for our URL shortener BASE62_ALPHABET = string.ascii_letters + string.digits DEFAULT_SHORT_LENGTH = 7 DEFAULT_DOMAIN = "https://sho.rt/" @dataclass(frozen=True) class ShortenedURL: """Immutable representation of a shortened URL entry.""" original: str code: str domain: str @property def short_url(self) -> str: # Combine the domain with the code to form full short URL return f"{self.domain}{self.code}" class AbstractURLStorage(ABC): """Abstract base class for URL storage backends.""" @abstractmethod def save(self, code: str, url: str) -> None: pass @abstractmethod def lookup(self, code: str) -> Optional[str]: pass @abstractmethod def exists(self, code: str) -> bool: pass class InMemoryURLStorage(AbstractURLStorage): """In-memory implementation of URL storage.""" def init(self) -> None: self._store: Dict[str, str] = {} def save(self, code: str, url: str) -> None: self._store[code] = url def lookup(self, code: str) -> Optional[str]: return self._store.get(code) def exists(self, code: str) -> bool: return code in self._store class URLShortener: """Main URL shortener service.""" def init(self, storage: AbstractURLStorage, domain: str = DEFAULT_DOMAIN) -> None: self._storage = storage self._domain = domain def _generate_code(self, url: str, salt: int = 0) -> str: # Hash the URL to produce a deterministic code raw = f"{url}{salt}".encode("utf-8") digest = hashlib.sha256(raw).digest() # Convert digest bytes to base62 num = int.from_bytes(digest, "big") code_chars = [] while num > 0 and len(code_chars) < DEFAULT_SHORT_LENGTH: num, rem = divmod(num, 62) code_chars.append(BASE62_ALPHABET[rem]) return "".join(code_chars) def shorten(self, url: str) -> ShortenedURL: if not isinstance(url, str): raise TypeError("URL must be a string") if not url: raise ValueError("URL cannot be empty") # Handle collisions by re-salting salt = 0 code = self._generate_code(url, salt) while self._storage.exists(code) and self._storage.lookup(code) != url: salt += 1 code = self._generate_code(url, salt) self._storage.save(code, url) return ShortenedURL(original=url, code=code, domain=self._domain) def expand(self, code: str) -> str: result = self._storage.lookup(code) if result is None: raise KeyError(f"No URL found for code: {code}") return result if name == "main": shortener = URLShortener(InMemoryURLStorage()) entry = shortener.shorten("https://www.example.com/some/very/long/path") print(f"Short URL: {entry.short_url}") print(f"Expanded: {shortener.expand(entry.code)}") Code Review 1. Lines 26-39. AbstractURLStorage with one concrete implementation. We are clearly preparing for the imaginary day someone writes a Redis backend. Until then it is just ceremony. 2. Lines 13-23. Frozen dataclass with a single computed property. A tuple would have done the job, but sure, let's bring the whole dataclass machinery in. 3. Line 66. Comment 'Hash the URL to produce a deterministic code' restates the next line. Thanks for the narration. 4. Lines 71-74. This base62 loop silently truncates the number once we hit DEFAULT_SHORT_LENGTH, which means codes can be shorter than expected for small hashes and you are throwing away most of the entropy. Probably fine, but worth a comment that is not just restating the code. 5. Lines 78-79. isinstance check on a parameter already typed as str. If we trusted the type hints we would not need this, and if we did not trust them we should not have written them. 6. Lines 83-85. Collision handling against SHA-256 truncated to 7 base62 chars. The probability is nonzero, sure, but the salt-and-retry loop feels like overkill for what is effectively a toy. 7. Line 60. Single underscore prefix on _storage and _domain in a class with no inheritance story. The privacy theater continues.

Discussion in the ATmosphere

Loading comments...