Skip to Content
Data Models (2)

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:

FieldTypeRequiredDescription
shortCodestringYesUnique alphanumeric identifier (6-20 chars)
longUrlstringYesOriginal URL to redirect to
createdAtstringYesISO 8601 timestamp of creation
expiresAtstringNoISO 8601 timestamp when URL expires
clicksnumberYesTotal number of redirects/clicks
lastAccessedAtstringNoISO 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:

  • shortCode must be unique across all URLs
  • longUrl must be a valid URL (validated by Zod)
  • createdAt is set automatically on creation
  • clicks starts at 0
  • lastAccessedAt is updated on each redirect
  • expiresAt must 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:

FieldTypeRequiredDescription
longUrlstringYesThe URL to shorten
customCodestringNoUser-specified short code
expiresAtstringNoExpiration timestamp (ISO 8601)

Validation Rules:

longUrl:

  • Must be a valid URL format
  • Must include protocol (http:// or https://)
  • Examples: ✅ https://example.comexample.com

customCode:

  • Length: 4-20 characters
  • Pattern: Alphanumeric only (/^[a-zA-Z0-9]+$/)
  • Must be unique (not already in use)
  • Examples: ✅ githubmylink123my-linkab

expiresAt:

  • Must be ISO 8601 datetime format
  • Must be in the future
  • Examples: ✅ 2024-12-31T23:59:59.000Z2020-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:

FieldTypeRequiredDescription
longUrlstringYesNew destination URL

Notes:

  • Only the longUrl can be updated
  • The shortCode cannot be changed
  • clicks and timestamps are preserved
  • expiresAt cannot 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:

FieldTypeDescription
totalUrlsnumberTotal number of URLs in the system
totalClicksnumberSum of all clicks across all URLs
activeUrlsnumberCount of non-expired URLs
expiredUrlsnumberCount 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:

FieldTypeDescription
urlsShortUrl[]Array of URL objects for current page
totalnumberTotal number of URLs (all pages)
pagenumberCurrent page number (1-indexed)
limitnumberNumber of items per page
hasMorebooleanWhether 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 1704067199

Error 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