Skip to Content
Architecture Overview

Source: Jolli-sample-repos/url-shortener  Last Updated: 4/8/2026


Architecture Overview

This document provides a high-level overview of the URL Shortener’s architecture, design decisions, and system components.

System Architecture

The URL Shortener follows a client-server architecture with a RESTful API backend and a simple web-based frontend.

┌─────────────┐ │ Browser │ │ (Frontend) │ └──────┬──────┘ │ HTTP/REST ┌──────▼──────────────────────┐ │ Express.js Backend │ │ ┌────────────────────┐ │ │ │ Routes Layer │ │ │ │ (routes.ts) │ │ │ └─────────┬──────────┘ │ │ │ │ │ ┌─────────▼──────────┐ │ │ │ Business Logic │ │ │ │ - Validation │ │ │ │ - Generator │ │ │ └─────────┬──────────┘ │ │ │ │ │ ┌─────────▼──────────┐ │ │ │ Storage Layer │ │ │ │ (In-Memory Map) │ │ │ └────────────────────┘ │ └─────────────────────────────┘

Core Components

1. Entry Point (backend/src/index.ts)

The main application entry point that:

  • Initializes the Express application
  • Configures middleware (CORS, JSON parsing, logging)
  • Registers API routes
  • Sets up the redirect endpoint
  • Configures Swagger documentation
  • Starts the HTTP server

Key Responsibilities:

  • Application bootstrap
  • Middleware configuration
  • Error handling
  • Server lifecycle management
const app: Express = express(); const PORT = process.env.PORT || 3001; // Middleware app.use(cors()); app.use(express.json()); // Routes app.use('/api/v1', routes); app.get('/r/:shortCode', redirectHandler); // Start server app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });

2. Routes Layer (backend/src/routes.ts)

Defines all API endpoints and their handlers. This layer is responsible for:

  • Request/response handling
  • Input validation using Zod schemas
  • Calling business logic
  • Formatting responses
  • HTTP status code management

Endpoint Categories:

  • URL Management (POST, GET, PUT, DELETE)
  • Statistics (GET /stats, GET /stats/top)
  • Redirect (GET /r/:shortCode)

3. Data Models (backend/src/models.ts)

TypeScript interfaces that define the shape of data throughout the application:

export interface ShortUrl { shortCode: string; longUrl: string; createdAt: string; expiresAt?: string; clicks: number; lastAccessedAt?: string; }

Why TypeScript interfaces?

  • Type safety at compile time
  • IDE autocomplete and IntelliSense
  • Self-documenting code
  • Easier refactoring

4. Storage Layer (backend/src/storage.ts)

Provides an abstraction over data persistence. Currently implements in-memory storage using JavaScript Maps.

Storage Structure:

class UrlStorage { private urls: Map<string, ShortUrl>; // shortCode -> ShortUrl private longUrlIndex: Map<string, string>; // longUrl -> shortCode }

Key Operations:

  • create(url) - Store a new short URL
  • findByShortCode(code) - Retrieve by short code
  • findByLongUrl(url) - Find existing short URL for a long URL
  • update(code, url) - Update an existing URL
  • delete(code) - Remove a URL
  • incrementClicks(code) - Track usage

Design Pattern: Repository Pattern

The storage layer abstracts data access, making it easy to swap implementations (e.g., switch from in-memory to database) without changing business logic.

5. Short Code Generator (backend/src/generator.ts)

Responsible for generating unique short codes using the nanoid library.

Algorithm:

  1. Generate a random 6-character alphanumeric code
  2. Check for collisions with existing codes
  3. Retry if collision detected (max 10 attempts)
  4. Throw error if unable to generate unique code
const ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; const DEFAULT_LENGTH = 6; export function generateShortCode(): string { let code: string; do { code = nanoid(); } while (urlStorage.exists(code)); return code; }

Collision Probability:

  • 62^6 = 56.8 billion possible combinations
  • With 1 million URLs, collision probability is extremely low (~0.0017%)

Custom Code Validation:

  • Length: 4-20 characters
  • Pattern: Alphanumeric only
  • Uniqueness check before creation

6. Validation Layer (backend/src/validator.ts)

Uses Zod for runtime schema validation of API requests.

export const createUrlSchema = z.object({ longUrl: z.string().url(), customCode: z.string() .regex(/^[a-zA-Z0-9]+$/) .min(4).max(20) .optional(), expiresAt: z.string().datetime().optional(), });

Benefits:

  • Type-safe validation
  • Automatic error messages
  • Runtime type checking
  • Integration with TypeScript

7. API Documentation (backend/src/swagger.ts)

Configures Swagger/OpenAPI documentation using JSDoc comments in the routes file.

Access: http://localhost:3001/api-docs 

Data Flow

Creating a Short URL

1. Client sends POST /api/v1/urls 2. Express middleware parses JSON body 3. Routes layer receives request 4. Validator validates request body (Zod) 5. Check if long URL already exists (deduplication) 6. Generate short code or validate custom code 7. Storage layer creates entry 8. Return short URL to client

Redirecting a Short URL

1. Client requests GET /r/AbC123 2. Express routes to redirect handler 3. Storage finds URL by short code 4. Check if URL exists (404 if not) 5. Check if URL expired (410 if yes) 6. Increment click counter 7. Return 302 redirect to long URL

Design Decisions

1. In-Memory Storage

Chosen Approach: JavaScript Map for storage

Pros:

  • Fast lookups (O(1))
  • No external dependencies
  • Simple to implement
  • Great for development and testing

Cons:

  • Data lost on restart
  • Limited by available RAM
  • No persistence
  • Not suitable for production at scale

Future: Abstract storage layer allows easy migration to databases (MongoDB, PostgreSQL, Redis).

2. RESTful API Design

Why REST?

  • Standard HTTP methods (GET, POST, PUT, DELETE)
  • Stateless communication
  • Cacheable responses
  • Wide tooling support
  • Easy to document and test

Endpoint Structure:

POST /api/v1/urls - Create GET /api/v1/urls/:code - Read one GET /api/v1/urls - Read all (paginated) PUT /api/v1/urls/:code - Update DELETE /api/v1/urls/:code - Delete GET /r/:code - Redirect

3. nanoid for Short Code Generation

Why nanoid over UUID?

  • Shorter codes (6 chars vs 36)
  • URL-safe by default
  • Cryptographically secure
  • Fast performance
  • Customizable alphabet

Alternatives Considered:

  • Base62 encoding of auto-increment ID (predictable)
  • MD5/SHA hash truncation (longer, not guaranteed unique)
  • UUID (too long for “short” URL)

4. TypeScript

Benefits:

  • Catch errors at compile time
  • Better IDE support
  • Self-documenting code via types
  • Easier refactoring
  • Improved maintainability

5. Zod for Validation

Why Zod over alternatives?

  • TypeScript-first design
  • Runtime and compile-time safety
  • Composable schemas
  • Excellent error messages
  • Type inference

6. Express.js Framework

Why Express?

  • Minimal and flexible
  • Large ecosystem
  • Well-documented
  • Industry standard
  • Easy to learn

7. Stateless Design

The API is completely stateless - each request contains all necessary information. This enables:

  • Horizontal scaling
  • Load balancing
  • No session management
  • Simpler deployment

Performance Considerations

Time Complexity

OperationComplexityNotes
Create URLO(1)Map insertion
Find by short codeO(1)Direct Map lookup
Find by long URLO(1)Secondary index
List all URLsO(n)Iterate all entries
Update URLO(1)Map update
Delete URLO(1)Map deletion
Increment clicksO(1)Direct update

Scalability

Current Limitations:

  • Single server (no horizontal scaling yet)
  • In-memory storage (limited capacity)
  • No caching layer
  • No rate limiting

Scaling Strategies:

  1. Database Migration: Move to PostgreSQL/MongoDB for persistence
  2. Caching: Add Redis for frequently accessed URLs
  3. CDN: Serve frontend assets via CDN
  4. Load Balancing: Multiple backend instances behind load balancer
  5. Rate Limiting: Prevent abuse with request limits

Security Considerations

Current Implementation:

  • CORS enabled for cross-origin requests
  • Input validation via Zod
  • URL validation prevents invalid URLs
  • Custom code sanitization

Recommended Additions:

  • Rate limiting per IP
  • Authentication for management endpoints
  • HTTPS in production
  • Input sanitization for XSS prevention
  • SQL injection prevention (when using SQL databases)
  • DDoS protection

Error Handling

The application uses HTTP status codes appropriately:

Status CodeMeaningUse Case
200OKSuccessful retrieval
201CreatedNew URL created
204No ContentSuccessful deletion
400Bad RequestValidation error
404Not FoundShort code doesn’t exist
410GoneURL expired
500Server ErrorUnexpected error

Frontend Architecture

The frontend is intentionally simple:

  • No Build Process: Vanilla JavaScript, no bundlers
  • No Framework: Pure DOM manipulation
  • Responsive Design: CSS Grid and Flexbox
  • Fetch API: For HTTP requests

File Structure:

frontend/ ├── index.html # Main page ├── css/ │ └── styles.css # Styling └── js/ └── app.js # Application logic

Next Steps