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
Functional Requirements
Non-Functional Requirements
API Design
Database Schema
High-Level Design (HLD)
Deep Dive into HLD
Trade-Offs
Example Code
Functional Requirements
Shorten URL: Generate a short URL for any given long URL.
Redirect: Redirect users to the original URL when the short URL is accessed.
Custom Alias (Optional): Allow users to specify a custom short URL.
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 TypeDescriptionid
BIGINTAuto-increment primary key.short_url
VARCHAR(10)Unique short identifier.long_url
TEXTOriginal long URL.custom_alias
VARCHAR(10)Custom short URL (if provided).creation_date
TIMESTAMPTimestamp of URL creation.expiration_date
TIMESTAMPExpiration date (if applicable).click_count
INTAccess 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:
A user submits a long URL via the frontend.
The backend checks if the long URL already exists in the database.
If it does, the existing short URL is returned to the user.
If not, a new short URL is generated using the
shortid
library or another unique ID method.The mapping of the long URL to the short URL is saved in the database.
- Redirection:
When a user visits a short URL, the backend looks up the corresponding long URL in the database.
If the mapping is found, the server issues a
302 Redirect
to send the user to the original long URL.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:
The user enters a long URL in an input box.
When the “Shorten” button is clicked, the frontend sends a POST request to the backend with the long URL.
On success, the backend responds with the generated short URL.
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
, andclickCount
.
4. Lookup Process:
When a short URL is accessed, the database is queried to find the corresponding long URL.
If a match is found, the long URL is returned to complete the redirection.
High-Level Design (HLD)
Components
API Gateway: Manages incoming API requests.
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
- 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
Hash Collisions: Avoid by checking for uniqueness during URL generation.
Performance vs Consistency: Caching improves speed but may lead to stale data.
Custom Alias Conflicts: Ensure alias uniqueness during creation.
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
Analytics: Implement user engagement tracking (e.g., clicks, access times).
Expiration: Add expiry for short URLs to optimize storage.
Custom Alias: Allow user-defined short URLs.
Deployment: Host the application on AWS or Vercel for global availability.
Authentication: Integrate OAuth for user-specific URL management.
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! 😊