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 URLfindByShortCode(code)- Retrieve by short codefindByLongUrl(url)- Find existing short URL for a long URLupdate(code, url)- Update an existing URLdelete(code)- Remove a URLincrementClicks(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:
- Generate a random 6-character alphanumeric code
- Check for collisions with existing codes
- Retry if collision detected (max 10 attempts)
- 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 clientRedirecting 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 URLDesign 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 - Redirect3. 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
| Operation | Complexity | Notes |
|---|---|---|
| Create URL | O(1) | Map insertion |
| Find by short code | O(1) | Direct Map lookup |
| Find by long URL | O(1) | Secondary index |
| List all URLs | O(n) | Iterate all entries |
| Update URL | O(1) | Map update |
| Delete URL | O(1) | Map deletion |
| Increment clicks | O(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:
- Database Migration: Move to PostgreSQL/MongoDB for persistence
- Caching: Add Redis for frequently accessed URLs
- CDN: Serve frontend assets via CDN
- Load Balancing: Multiple backend instances behind load balancer
- 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 Code | Meaning | Use Case |
|---|---|---|
| 200 | OK | Successful retrieval |
| 201 | Created | New URL created |
| 204 | No Content | Successful deletion |
| 400 | Bad Request | Validation error |
| 404 | Not Found | Short code doesn’t exist |
| 410 | Gone | URL expired |
| 500 | Server Error | Unexpected 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 logicNext Steps
- Tech Stack Details - Deep dive into technologies
- Data Models - Detailed data structure documentation
- API Reference - Complete API documentation
- Development Guide - Start contributing