Design a URL Shortener (e.g., TinyURL)

Design a URL Shortener (e.g., TinyURL)

A URL shortener is a tool that converts lengthy URLs into short, shareable links while maintaining the functionality of redirecting users to the original URL. Popular examples include bit.ly and tinyurl.com. In this guide, we’ll explore the process of designing a scalable and efficient URL shortener.

Link to My LinkLite: Live Link

Contents

  1. Functional Requirements

  2. Non-Functional Requirements

  3. API Design

  4. Database Schema

  5. High-Level Design (HLD)

  6. Deep Dive into HLD

  7. Trade-Offs

  8. Example Code

Functional Requirements

  1. Shorten URL: Generate a short URL for any given long URL.

  2. Redirect: Redirect users to the original URL when the short URL is accessed.

  3. Custom Alias (Optional): Allow users to specify a custom short URL.

  4. Expiration (Optional): Support URLs with expiration times.

Non-Functional Requirements

  • Scalability: Handle millions of URLs and high traffic volumes.

  • Low Latency: Ensure fast redirection and URL shortening.

  • High Availability: Keep the service operational 24/7.

  • Reliability: Prevent loss of URLs.

  • Security: Safeguard against malicious URL usage.

API Design

1. Shorten URL API

Request:

POST /shorten
{
  "longUrl": "https://example.com/long/path",
  "customAlias": "example" // Optional
}

Response:

{
  "shortUrl": "https://tinyurl.com/example"
}

2. Redirect API

Request:

GET /{shortUrlId}

Response:
302 Redirect to the original URL.

3. URL Analytics (Optional)

Request:

GET /analytics/{shortUrlId}

Response:

{
  "clicks": 12345,
  "creationDate": "2024-01-01T00:00:00Z",
  "lastAccessed": "2024-02-01T12:00:00Z"
}

Database Schema

Column NameData TypeDescriptionidBIGINTAuto-increment primary key.short_urlVARCHAR(10)Unique short identifier.long_urlTEXTOriginal long URL.custom_aliasVARCHAR(10)Custom short URL (if provided).creation_dateTIMESTAMPTimestamp of URL creation.expiration_dateTIMESTAMPExpiration date (if applicable).click_countINTAccess count for the short URL.

How It Works

1. Backend

The backend is responsible for handling the logic and database operations for URL shortening and redirection.

  • URL Shortening:
  1. A user submits a long URL via the frontend.

  2. The backend checks if the long URL already exists in the database.

  3. If it does, the existing short URL is returned to the user.

  4. If not, a new short URL is generated using the shortid library or another unique ID method.

  5. The mapping of the long URL to the short URL is saved in the database.

  • Redirection:
  1. When a user visits a short URL, the backend looks up the corresponding long URL in the database.

  2. If the mapping is found, the server issues a 302 Redirect to send the user to the original long URL.

  3. If no mapping is found, the backend returns a 404 Not Found error.

2. Frontend

The frontend provides a user-friendly interface for interacting with the URL shortener.

  • Shorten URL Workflow:
  1. The user enters a long URL in an input box.

  2. When the “Shorten” button is clicked, the frontend sends a POST request to the backend with the long URL.

  3. On success, the backend responds with the generated short URL.

  4. The short URL is displayed on the page, allowing the user to copy or open it.

  • Redirection:

  • When a user clicks a short URL, their browser automatically sends a request to the backend, triggering the redirection process.

3. Database

The database acts as the central store for URL mappings and other related data.

  • Stores records with:

  • longUrl: The original URL.

  • shortUrl: The generated short URL.

  • Optional fields like customAlias, expirationDate, and clickCount.

4. Lookup Process:

  1. When a short URL is accessed, the database is queried to find the corresponding long URL.

  2. If a match is found, the long URL is returned to complete the redirection.

High-Level Design (HLD)

Components

  1. API Gateway: Manages incoming API requests.

  2. URL Shortening Service:

  • Generates short URLs using hash functions or unique IDs.

  • Stores mappings in the database.

3. Redirection Service: Redirects short URLs to their corresponding long URLs.

4. Database: Holds all URL mappings.

5. Cache (Optional): Speeds up lookups for frequently accessed URLs.

Deep Dive into HLD

URL Generation Methods

  • Hash-Based: Use algorithms like MD5 or SHA-256 for generating fixed-length identifiers.

  • Counter-Based: Maintain a global counter, encode it using Base62, and use it as the short URL.

  • Custom Alias: Validate and use user-provided aliases.

Workflow

  1. Shorten URL:
  • Check if the long URL already exists in the database.

  • If not, generate a unique short URL and store the mapping.

2. Redirect:

  • Retrieve the long URL from the database or cache using the short URL.

  • Redirect to the long URL.

3. Cache Management: Frequently accessed short URLs are stored in the cache.

Trade-Offs

  1. Hash Collisions: Avoid by checking for uniqueness during URL generation.

  2. Performance vs Consistency: Caching improves speed but may lead to stale data.

  3. Custom Alias Conflicts: Ensure alias uniqueness during creation.

  4. Database Scalability: Use sharding or distributed systems for high availability.

Code Implementation

"use client";
import axios from "axios";
import React, { useEffect, useRef, useState } from "react";
import { FaClipboard, FaClipboardCheck } from "react-icons/fa";

const UrlComponent = () => {
  const searchInputRef = useRef<HTMLInputElement>(null);
  const [shortUrl, setShortUrl] = useState("");
  const [originalUrl, setOriginalUrl] = useState("");
  const [isCopied, setIsCopied] = useState(false);
  const url= "http://localhost:3000"
  interface ShortenResponse {
    shortUrl: string;
  }
  const focusField = (e: KeyboardEvent) => {
    if (e.ctrlKey && e.key === "k") {
      e.preventDefault();
      searchInputRef.current?.focus();
    }
  };
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    try {
      const response = await axios.post<ShortenResponse>(
        `${url}/api/shortner`,
        {
          originalUrl,
        }
      );
      setShortUrl(response.data.shortUrl);
      setIsCopied(false); // Reset copied status when a new URL is generated
    } catch (error) {
      console.error("Error creating short URL", error);
    }
  };
  const handleCopy = () => {
    if (shortUrl) {
      navigator.clipboard.writeText(shortUrl);
      setIsCopied(true);
      // Reset copied status after 2 seconds
      setTimeout(() => {
        setIsCopied(false);
      }, 2000);
    }
  };
  useEffect(() => {
    const handleKeydown = (e: KeyboardEvent) => {
      if (e.ctrlKey && e.key === "k") {
        focusField(e);
      }
    };
    window.addEventListener("keydown", handleKeydown);
    return () => {
      window.removeEventListener("keydown", handleKeydown);
    };
  }, []);
  return (
    <div className="bg-white p-6 rounded-lg shadow-lg w-full max-w-lg">
      <h1 className="text-3xl font-bold text-gray-900 text-center mb-6">
        URL Shortener
      </h1>
      <form onSubmit={handleSubmit} className="space-y-4">
        <input
          type="url"
          ref={searchInputRef}
          placeholder="Enter your long URL (CTRL + K)"
          className="w-full px-4 py-2 border border-gray-300 rounded-md 
focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-800"
          value={originalUrl}
          onChange={(e) => setOriginalUrl(e.target.value)}
          required
        />
        <button
          type="submit"
          className="w-full bg-blue-600 text-white py-2 rounded-md 
hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:outline-none 
transition duration-200"
        >
          Shorten URL
        </button>
      </form>
      {shortUrl && (
        <div className="mt-6 text-center space-y-2">
          <p className="text-gray-700 font-medium">Your Shortened URL:</p>
          <div className="flex items-center justify-center space-x-2">
            <a
              href={shortUrl}
              target="_blank"
              rel="noopener noreferrer"
              className="text-blue-600 font-bold hover:underline break-all"
            >
              {shortUrl}
            </a>
            <button
              onClick={handleCopy}
              className="text-gray-500 hover:text-blue-500 focus:outline-none
transition duration-200"
              title="Copy to clipboard"
            >
              {isCopied ? (
                <FaClipboardCheck className="text-green-500" size={20} />
              ) : (
                <FaClipboard size={20} />
              )}
            </button>
          </div>
          {isCopied && (
            <p className="text-sm text-green-500">Copied to clipboard!</p>
          )}
        </div>
      )}
    </div>
  );
};
export default UrlComponent;

Backend (Next.JS)

import { nanoid } from "nanoid";
import { NextResponse } from "next/server";

type UrlData = {
  originalUrl: string;
  creationTime: number;
};
const urlGraph = new Map<string, UrlData>();
const baseUrl = "http://localhost:3000";
let numberOfRequests: { [key: string]: number } = {};
// Reset rate-limiting data every second
setInterval(() => {
  numberOfRequests = {};
}, 1000);
export async function POST(req: Request) {
  const { originalUrl } = await req.json();
  // Validate URL
  if (!originalUrl || typeof originalUrl !== "string") {
    return NextResponse.json(
      { message: "Invalid or missing URL" },
      { status: 400 }
    );
  }
  // Check if URL is valid using the URL constructor
  let validatedUrl: URL;
  try {
    validatedUrl = new URL(originalUrl);
  } catch {
    return NextResponse.json(
      { message: "Invalid URL format" },
      { status: 400 }
    );
  }
  // Ensure the URL has http:// or https://
  if (!/^https?:\/\//i.test(validatedUrl.href)) {
    validatedUrl = new URL(`http://${originalUrl}`);
  }
  // Rate limiting
  if (numberOfRequests[validatedUrl.href]) {
    if (numberOfRequests[validatedUrl.href] >= 5) {
      return NextResponse.json(
        { message: "Too many requests. Try again later." },
        { status: 429 }
      );
    }
    numberOfRequests[validatedUrl.href]++;
  } else {
    numberOfRequests[validatedUrl.href] = 1;
  }
  // Generate a unique short ID
  const shortId = nanoid(6);
  const creationTime = Date.now();
  urlGraph.set(shortId, { originalUrl: validatedUrl.href, creationTime });
  return NextResponse.json({
    shortUrl: `${baseUrl}/api/${shortId}`,
  });
}
export async function GET(req: Request) {
  const url = new URL(req.url);
  const id = url.pathname.split("/").pop();
  if (id && urlGraph.has(id)) {
    const { originalUrl, creationTime } = urlGraph.get(id)!;
    const currentTime = Date.now();
    // Check expiration (30 minutes)
    if (currentTime - creationTime > 30 * 60 * 1000) {
      urlGraph.delete(id); // Remove expired URL
      return NextResponse.json({ message: "URL has expired" }, { status: 410 });
    }
    return NextResponse.redirect(originalUrl);
  }
  return NextResponse.json({ message: "URL not found" }, { status: 404 });
}

Enhancements

  1. Analytics: Implement user engagement tracking (e.g., clicks, access times).

  2. Expiration: Add expiry for short URLs to optimize storage.

  3. Custom Alias: Allow user-defined short URLs.

  4. Deployment: Host the application on AWS or Vercel for global availability.

  5. Authentication: Integrate OAuth for user-specific URL management.

  6. UI Enhancements: Add features like QR code generation.

Conclusion

Designing a URL shortener involves understanding functional and non-functional requirements, creating an efficient database schema, and implementing scalable architecture. By using a structured approach, you can build a reliable and high-performing system. This design can be further enhanced with analytics, security measures, and optimization techniques.

If you found this blog post helpful, please consider sharing it with others who might benefit. Follow me for more insightful content on JavaScript, React, and other web development topics.

Connect with me on Twitter, LinkedIn, and GitHub for updates and more discussions.

Thank you for reading! 😊