Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

3 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

URL Shortener

A production-ready URL shortener service built with Node.js, demonstrating key Low-Level Design patterns and SOLID principles.

🎯 Project Overview

This project showcases a complete URL shortener implementation following industry-standard design patterns including Strategy Pattern, Repository Pattern, Dependency Injection, and Layered Architecture.

Key Features

  • βœ… URL shortening with customizable strategies
  • βœ… Redis caching for high performance
  • βœ… Click tracking and analytics
  • βœ… MongoDB for persistent storage
  • βœ… RESTful API design
  • βœ… Extensible architecture

πŸ—οΈ Architecture & Design Patterns

1. Layered Architecture

The application follows a clear separation of concerns with distinct layers:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         Routes Layer                β”‚  ← HTTP endpoints
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚       Controller Layer              β”‚  ← Request handling
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚        Service Layer                β”‚  ← Business logic
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚      Repository Layer               β”‚  ← Data access
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚    Model Layer + Database           β”‚  ← Data persistence
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Benefits:

  • Clear separation of concerns
  • Easy to test individual layers
  • Maintainable and scalable code

2. Strategy Pattern 🎨

The Strategy Pattern allows different algorithms for generating short codes to be swapped at runtime without modifying the service logic.

Implementation:

Base Strategy (Interface):

// src/strategies/shortCode.strategy.js
class ShortCodeStrategy {
  generate() {
    throw new Error("generate() must be implemented");
  }
}

Concrete Strategy:

// src/strategies/base62.strategy.js
class Base62Strategy extends ShortCodeStrategy {
  generate() {
    return nanoid(7); // Generates URL-safe short codes
  }
}

Usage in Service:

class UrlService {
  constructor(shortCodeStrategy) {
    this.shortCodeStrategy = shortCodeStrategy; // Strategy injection
  }

  async shorten(originalUrl) {
    const shortCode = this.shortCodeStrategy.generate(); // Uses injected strategy
    // ... rest of logic
  }
}

Benefits:

  • Open/Closed Principle: Open for extension, closed for modification
  • Easy to add new short code generation algorithms (MD5, Base62, Sequential, etc.)
  • No need to modify existing service code when adding new strategies

Example: Adding a New Strategy

// src/strategies/sequential.strategy.js
class SequentialStrategy extends ShortCodeStrategy {
  constructor() {
    super();
    this.counter = 1000;
  }
  
  generate() {
    return `URL${this.counter++}`;
  }
}

// Usage in routes
const service = new UrlService(new SequentialStrategy());

3. Repository Pattern πŸ—„οΈ

Abstracts data access logic from business logic, making the codebase database-agnostic.

// src/repositories/url.repository.js
class UrlRepository {
  async create(data) {
    return Url.create(data);
  }

  async findByCode(code) {
    return Url.findOne({ shortCode: code });
  }

  async incrementClicks(code) {
    return Url.updateOne(
      { shortCode: code },
      { $inc: { clickCount: 1 } }
    );
  }
}

Benefits:

  • Centralizes all database queries
  • Easy to switch databases (MongoDB β†’ PostgreSQL)
  • Simplifies unit testing with mock repositories
  • Clear separation between data access and business logic

4. Dependency Injection πŸ’‰

Dependencies are injected rather than hard-coded, improving testability and flexibility.

// Routes file - Dependency injection at startup
const service = new UrlService(new Base62Strategy());
const controller = new UrlController(service);

Benefits:

  • Loose coupling between components
  • Easy to test (inject mock dependencies)
  • Flexible configuration (swap implementations easily)

5. Caching Strategy ⚑

Implements cache-aside pattern with Redis for optimal performance:

async resolve(shortCode) {
  // 1. Try cache first
  const cachedUrl = await redisClient.get(shortCode);
  if (cachedUrl) {
    await urlRepository.incrementClicks(shortCode);
    return cachedUrl;
  }

  // 2. Cache miss - query database
  const url = await urlRepository.findByCode(shortCode);
  if (!url) return null;

  // 3. Update cache for future requests
  await redisClient.set(shortCode, url.originalUrl);
  await urlRepository.incrementClicks(shortCode);
  return url.originalUrl;
}

Benefits:

  • Reduces database load
  • Improves response time
  • Scalable for high traffic

πŸ“ Project Structure

nodejs-lld-url-shortener/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ app.js                      # Express app setup
β”‚   β”œβ”€β”€ server.js                   # Server entry point
β”‚   β”œβ”€β”€ config/
β”‚   β”‚   β”œβ”€β”€ mongo.js                # MongoDB connection
β”‚   β”‚   └── redis.js                # Redis connection
β”‚   β”œβ”€β”€ controllers/
β”‚   β”‚   └── url.controller.js       # HTTP request handlers
β”‚   β”œβ”€β”€ services/
β”‚   β”‚   └── url.service.js          # Business logic layer
β”‚   β”œβ”€β”€ repositories/
β”‚   β”‚   └── url.repository.js       # Data access layer
β”‚   β”œβ”€β”€ models/
β”‚   β”‚   └── url.model.js            # Mongoose schema
β”‚   β”œβ”€β”€ strategies/
β”‚   β”‚   β”œβ”€β”€ shortCode.strategy.js   # Base strategy (interface)
β”‚   β”‚   └── base62.strategy.js      # Concrete strategy
β”‚   └── routes/
β”‚       └── url.routes.js           # API routes
β”œβ”€β”€ .env                            # Environment variables
β”œβ”€β”€ package.json
└── README.md

πŸš€ Getting Started

Prerequisites

  • Node.js (v14 or higher)
  • Either:
    • Docker and Docker Compose (recommended)
    • OR MongoDB (running on default port 27017) + Redis (running on default port 6379)

Installation

  1. Clone the repository:

    git clone <repository-url>
    cd nodejs-lld-url-shortener
  2. Install dependencies:

    npm install
  3. Configure environment variables: Create a .env file in the root directory:

    MONGO_URI=mongodb://127.0.0.1:27017/url_shortener
    REDIS_URL=redis://127.0.0.1:6379
  4. Start MongoDB and Redis:

    Option A: Using Docker (Recommended):

    # Start MongoDB and Redis containers
    docker-compose up -d
    
    # Stop containers
    docker-compose down
    
    # Stop and remove data volumes
    docker-compose down -v

    Option B: Local Installation:

    # Windows - MongoDB
    mongod
    
    # Windows - Redis
    redis-server
    
    # macOS/Linux - MongoDB
    sudo systemctl start mongod
    
    # macOS/Linux - Redis
    sudo systemctl start redis
  5. Run the application:

    npm run dev

    Server will start at http://localhost:3000


πŸ“‘ API Endpoints

1. Shorten URL

POST /shorten

Request Body:

{
  "originalUrl": "https://www.example.com/very/long/url"
}

Response:

{
  "shortUrl": "http://localhost:3000/ADsZo3D"
}

cURL Example:

curl -X POST http://localhost:3000/shorten \
  -H "Content-Type: application/json" \
  -d '{"originalUrl":"https://google.com"}'

2. Redirect to Original URL

GET /:code

Example:

GET http://localhost:3000/ADsZo3D
β†’ Redirects to https://www.example.com/very/long/url
β†’ Increments click count

Browser/cURL:

curl -L http://localhost:3000/ADsZo3D

πŸ”§ Tech Stack

Technology Purpose
Node.js Runtime environment
Express.js Web framework
MongoDB Primary database (persistence)
Mongoose MongoDB ODM
Redis Caching layer
nanoid Short code generation
dotenv Environment configuration

πŸ§ͺ Design Principles Applied

SOLID Principles

  1. Single Responsibility Principle (SRP)

    • Each class has one reason to change
    • Controller β†’ handles HTTP
    • Service β†’ business logic
    • Repository β†’ data access
  2. Open/Closed Principle (OCP)

    • Strategy pattern allows adding new strategies without modifying service
  3. Liskov Substitution Principle (LSP)

    • Any ShortCodeStrategy implementation can replace another
  4. Interface Segregation Principle (ISP)

    • Minimal, focused interfaces (ShortCodeStrategy has only generate())
  5. Dependency Inversion Principle (DIP)

    • High-level modules depend on abstractions (strategies), not concrete implementations

πŸŽ“ Extending the System

Adding a Custom Strategy

  1. Create new strategy file:

    // src/strategies/md5.strategy.js
    const crypto = require('crypto');
    const ShortCodeStrategy = require('./shortCode.strategy');
    
    class MD5Strategy extends ShortCodeStrategy {
      generate() {
        const hash = crypto.createHash('md5')
          .update(Date.now().toString())
          .digest('hex');
        return hash.substring(0, 7);
      }
    }
    
    module.exports = MD5Strategy;
  2. Use in routes:

    const MD5Strategy = require('../strategies/md5.strategy');
    const service = new UrlService(new MD5Strategy());

Adding Analytics Features

Extend the repository with new methods:

// In url.repository.js
async getAnalytics(code) {
  return Url.findOne({ shortCode: code })
    .select('originalUrl clickCount createdAt');
}

Add corresponding service and controller methods.


πŸ“Š Database Schema

URL Model

{
  originalUrl: String (required),
  shortCode: String (unique),
  expiresAt: Date (optional),
  clickCount: Number (default: 0),
  createdAt: Date (auto),
  updatedAt: Date (auto)
}

πŸ€” Why This Design?

Scalability

  • Redis caching reduces database load
  • Stateless services enable horizontal scaling
  • Repository pattern allows database sharding

Maintainability

  • Clear separation of concerns
  • Each component is independently testable
  • New features can be added without breaking existing code

Testability

  • Dependency injection enables easy mocking
  • Each layer can be unit tested in isolation

Extensibility

  • Strategy pattern allows new algorithms
  • Repository pattern allows database switching
  • Layered architecture supports feature additions

πŸ“ License

MIT License - Feel free to use this for learning and interviews!

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages