Source: Jolli-sample-repos/url-shortener Last Updated: 4/8/2026
Data Models
Complete reference for all data structures and interfaces used in the URL Shortener application.
Overview
The application uses TypeScript interfaces to define data models. These models are defined in backend/src/models.ts and used throughout the application for type safety.
Core Models
ShortUrl
The primary data model representing a shortened URL entry.
Location: backend/src/models.ts:5
export interface ShortUrl {
shortCode: string;
longUrl: string;
createdAt: string;
expiresAt?: string;
clicks: number;
lastAccessedAt?: string;
}Fields:
| Field | Type | Required | Description |
|---|---|---|---|
shortCode | string | Yes | Unique alphanumeric identifier (6-20 chars) |
longUrl | string | Yes | Original URL to redirect to |
createdAt | string | Yes | ISO 8601 timestamp of creation |
expiresAt | string | No | ISO 8601 timestamp when URL expires |
clicks | number | Yes | Total number of redirects/clicks |
lastAccessedAt | string | No | ISO 8601 timestamp of last access |
Example:
{
"shortCode": "AbC123",
"longUrl": "https://www.example.com/very/long/url/path",
"createdAt": "2024-01-15T10:30:00.000Z",
"expiresAt": "2024-12-31T23:59:59.000Z",
"clicks": 42,
"lastAccessedAt": "2024-01-15T14:20:00.000Z"
}Constraints:
shortCodemust be unique across all URLslongUrlmust be a valid URL (validated by Zod)createdAtis set automatically on creationclicksstarts at 0lastAccessedAtis updated on each redirectexpiresAtmust be a future datetime
CreateUrlRequest
Request payload for creating a new short URL.
Location: backend/src/models.ts:14
export interface CreateUrlRequest {
longUrl: string;
customCode?: string;
expiresAt?: string;
}Fields:
| Field | Type | Required | Description |
|---|---|---|---|
longUrl | string | Yes | The URL to shorten |
customCode | string | No | User-specified short code |
expiresAt | string | No | Expiration timestamp (ISO 8601) |
Validation Rules:
longUrl:
- Must be a valid URL format
- Must include protocol (http:// or https://)
- Examples: ✅
https://example.com❌example.com
customCode:
- Length: 4-20 characters
- Pattern: Alphanumeric only (
/^[a-zA-Z0-9]+$/) - Must be unique (not already in use)
- Examples: ✅
github✅mylink123❌my-link❌ab
expiresAt:
- Must be ISO 8601 datetime format
- Must be in the future
- Examples: ✅
2024-12-31T23:59:59.000Z❌2020-01-01T00:00:00.000Z
Example Request:
{
"longUrl": "https://github.com/yourcompany/repo",
"customCode": "github",
"expiresAt": "2024-12-31T23:59:59.000Z"
}UpdateUrlRequest
Request payload for updating an existing short URL.
Location: backend/src/models.ts:20
export interface UpdateUrlRequest {
longUrl: string;
}Fields:
| Field | Type | Required | Description |
|---|---|---|---|
longUrl | string | Yes | New destination URL |
Notes:
- Only the
longUrlcan be updated - The
shortCodecannot be changed clicksand timestamps are preservedexpiresAtcannot be updated (create new URL instead)
Example Request:
{
"longUrl": "https://example.com/new-destination"
}GlobalStats
Aggregate statistics for the entire URL shortener service.
Location: backend/src/models.ts:24
export interface GlobalStats {
totalUrls: number;
totalClicks: number;
activeUrls: number;
expiredUrls: number;
}Fields:
| Field | Type | Description |
|---|---|---|
totalUrls | number | Total number of URLs in the system |
totalClicks | number | Sum of all clicks across all URLs |
activeUrls | number | Count of non-expired URLs |
expiredUrls | number | Count of expired URLs |
Calculation:
const allUrls = urlStorage.findAll();
const stats: GlobalStats = {
totalUrls: allUrls.length,
totalClicks: allUrls.reduce((sum, url) => sum + url.clicks, 0),
activeUrls: allUrls.filter(url => !isExpired(url.expiresAt)).length,
expiredUrls: allUrls.filter(url => isExpired(url.expiresAt)).length,
};Example Response:
{
"totalUrls": 1523,
"totalClicks": 45829,
"activeUrls": 1489,
"expiredUrls": 34
}PaginatedResponse
Response wrapper for paginated list endpoints.
Location: backend/src/models.ts:31
export interface PaginatedResponse {
urls: ShortUrl[];
total: number;
page: number;
limit: number;
hasMore: boolean;
}Fields:
| Field | Type | Description |
|---|---|---|
urls | ShortUrl[] | Array of URL objects for current page |
total | number | Total number of URLs (all pages) |
page | number | Current page number (1-indexed) |
limit | number | Number of items per page |
hasMore | boolean | Whether more pages exist |
Pagination Logic:
const page = parseInt(req.query.page as string) || 1;
const limit = Math.min(parseInt(req.query.limit as string) || 10, 100);
const allUrls = urlStorage.findAll();
const total = allUrls.length;
const startIndex = (page - 1) * limit;
const endIndex = startIndex + limit;
const urls = allUrls.slice(startIndex, endIndex);
const response: PaginatedResponse = {
urls,
total,
page,
limit,
hasMore: endIndex < total,
};Example Response:
{
"urls": [
{
"shortCode": "AbC123",
"longUrl": "https://example.com/page1",
"clicks": 10,
"createdAt": "2024-01-01T12:00:00.000Z"
},
{
"shortCode": "DeF456",
"longUrl": "https://example.com/page2",
"clicks": 5,
"createdAt": "2024-01-01T13:00:00.000Z"
}
],
"total": 150,
"page": 1,
"limit": 10,
"hasMore": true
}Validation Schemas
Zod schemas provide runtime validation and are defined in backend/src/validator.ts.
createUrlSchema
Location: backend/src/validator.ts:7
export const createUrlSchema = z.object({
longUrl: z.string().url({ message: 'Must be a valid URL' }),
customCode: z
.string()
.regex(/^[a-zA-Z0-9]+$/, 'Custom code must be alphanumeric')
.min(4, 'Custom code must be at least 4 characters')
.max(20, 'Custom code must be at most 20 characters')
.optional(),
expiresAt: z.string().datetime({ message: 'Must be a valid ISO 8601 datetime' }).optional(),
});Validation Errors:
If validation fails, Zod returns structured errors:
{
"errors": [
{
"code": "invalid_string",
"message": "Must be a valid URL",
"path": ["longUrl"],
"validation": "url"
}
]
}updateUrlSchema
Location: backend/src/validator.ts:18
export const updateUrlSchema = z.object({
longUrl: z.string().url({ message: 'Must be a valid URL' }),
});Storage Schema
The in-memory storage uses two Map structures for efficient lookups.
Storage Structure
class UrlStorage {
// Primary storage: shortCode -> ShortUrl
private urls: Map<string, ShortUrl> = new Map();
// Secondary index: longUrl -> shortCode (for deduplication)
private longUrlIndex: Map<string, string> = new Map();
}Primary Map (urls):
- Key:
shortCode(string) - Value:
ShortUrl(object) - Purpose: Fast lookup by short code
Secondary Index (longUrlIndex):
- Key:
longUrl(string) - Value:
shortCode(string) - Purpose: Prevent duplicate long URLs
Time Complexity:
- Lookup by short code: O(1)
- Lookup by long URL: O(1)
- Insert: O(1)
- Update: O(1)
- Delete: O(1)
Database Schema (Future)
For production deployment with a database, here are recommended schemas:
PostgreSQL Schema
CREATE TABLE short_urls (
short_code VARCHAR(20) PRIMARY KEY,
long_url TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
expires_at TIMESTAMP,
clicks INTEGER NOT NULL DEFAULT 0,
last_accessed_at TIMESTAMP
);
-- Index for fast lookups by long URL
CREATE INDEX idx_long_url ON short_urls(long_url);
-- Index for finding expired URLs
CREATE INDEX idx_expires_at ON short_urls(expires_at) WHERE expires_at IS NOT NULL;
-- Index for analytics queries
CREATE INDEX idx_clicks ON short_urls(clicks DESC);MongoDB Schema
const shortUrlSchema = new Schema({
shortCode: {
type: String,
required: true,
unique: true,
index: true
},
longUrl: {
type: String,
required: true,
index: true
},
createdAt: {
type: Date,
required: true,
default: Date.now
},
expiresAt: {
type: Date,
index: true
},
clicks: {
type: Number,
required: true,
default: 0
},
lastAccessedAt: {
type: Date
}
});
// Compound index for pagination
shortUrlSchema.index({ createdAt: -1 });
// TTL index to auto-delete expired URLs
shortUrlSchema.index({ expiresAt: 1 }, { expireAfterSeconds: 0 });Redis Schema
# Key pattern: url:{shortCode}
# Value: JSON string of ShortUrl
SET url:AbC123 '{"shortCode":"AbC123","longUrl":"https://example.com","createdAt":"2024-01-01T12:00:00.000Z","clicks":5}'
# Secondary index: longurl:{hash(longUrl)} -> shortCode
SET longurl:sha256(longUrl) "AbC123"
# Sorted set for top URLs by clicks
ZADD url:clicks 5 "AbC123"
# Set expiration
EXPIREAT url:AbC123 1704067199Error Responses
Standard Error Format
interface ErrorResponse {
error: string;
errors?: Array<{
path: string[];
message: string;
}>;
}Examples:
404 Not Found:
{
"error": "Short URL not found"
}410 Gone (Expired):
{
"error": "Short URL has expired"
}400 Bad Request (Validation):
{
"errors": [
{
"path": ["customCode"],
"message": "Custom code must be at least 4 characters"
}
]
}400 Bad Request (Custom Code Taken):
{
"error": "Custom code is already in use"
}Type Inference
TypeScript automatically infers types from Zod schemas:
type CreateUrlData = z.infer<typeof createUrlSchema>;
// Equivalent to:
// type CreateUrlData = {
// longUrl: string;
// customCode?: string;
// expiresAt?: string;
// }This ensures consistency between runtime validation and compile-time types.
Next Steps
- API Reference - See how these models are used in endpoints
- Architecture Overview - Understand the overall system design
- Development Guide - Start working with the code