Serverless Link Management

Written by Terrell Flautt on October 4, 2025

· 6 min · snapiturl.com

Architecture

Frontend → CloudFront → API Gateway → Lambda
                    ↓
            DynamoDB + S3 Analytics
                    ↓
        Custom Domain + SSL Certificate

Core Challenge

Problem: High-performance URL shortening with analytics.
Solution: Edge-optimized redirects with real-time tracking.

URL Shortening Lambda

exports.createShortUrl = async (event) => {
    const { url, customSlug, userId } = JSON.parse(event.body);

    // Validate URL
    if (!isValidUrl(url)) {
        return {
            statusCode: 400,
            body: JSON.stringify({ error: 'Invalid URL' })
        };
    }

    // Generate or validate slug
    const slug = customSlug || generateSlug();

    // Check if slug exists
    const existing = await dynamoDB.get({
        TableName: 'short_urls',
        Key: { slug }
    }).promise();

    if (existing.Item) {
        return {
            statusCode: 409,
            body: JSON.stringify({ error: 'Slug already exists' })
        };
    }

    // Store URL mapping
    await dynamoDB.put({
        TableName: 'short_urls',
        Item: {
            slug,
            originalUrl: url,
            userId,
            createdAt: new Date().toISOString(),
            clicks: 0,
            isActive: true,
            expiresAt: getExpirationDate(),
            metadata: {
                userAgent: event.headers['User-Agent'],
                ip: event.requestContext.identity.sourceIp
            }
        }
    }).promise();

    return {
        statusCode: 201,
        body: JSON.stringify({
            shortUrl: `https://snapit.li/${slug}`,
            slug,
            originalUrl: url
        })
    };
};

function generateSlug() {
    const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    let result = '';
    for (let i = 0; i < 6; i++) {
        result += chars.charAt(Math.floor(Math.random() * chars.length));
    }
    return result;
}

Redirect Handler

exports.redirect = async (event) => {
    const slug = event.pathParameters.slug;

    // Get URL from DynamoDB
    const result = await dynamoDB.get({
        TableName: 'short_urls',
        Key: { slug }
    }).promise();

    if (!result.Item || !result.Item.isActive) {
        return {
            statusCode: 404,
            headers: { 'Content-Type': 'text/html' },
            body: '<h1>Link not found</h1>'
        };
    }

    // Track click analytics
    await trackClick(slug, event);

    // Increment click counter
    await dynamoDB.update({
        TableName: 'short_urls',
        Key: { slug },
        UpdateExpression: 'ADD clicks :inc SET lastClicked = :now',
        ExpressionAttributeValues: {
            ':inc': 1,
            ':now': new Date().toISOString()
        }
    }).promise();

    return {
        statusCode: 301,
        headers: {
            Location: result.Item.originalUrl,
            'Cache-Control': 'no-cache, no-store, must-revalidate'
        }
    };
};

async function trackClick(slug, event) {
    const clickData = {
        id: uuidv4(),
        slug,
        timestamp: Date.now(),
        ip: event.requestContext.identity.sourceIp,
        userAgent: event.headers['User-Agent'],
        referer: event.headers.Referer || 'direct',
        country: await getCountryFromIP(event.requestContext.identity.sourceIp)
    };

    await dynamoDB.put({
        TableName: 'click_analytics',
        Item: clickData
    }).promise();
}

Analytics Dashboard

exports.getAnalytics = async (event) => {
    const { slug, timeRange } = event.queryStringParameters;
    const userId = event.requestContext.authorizer.principalId;

    // Verify ownership
    const urlData = await dynamoDB.get({
        TableName: 'short_urls',
        Key: { slug }
    }).promise();

    if (urlData.Item.userId !== userId) {
        return {
            statusCode: 403,
            body: JSON.stringify({ error: 'Unauthorized' })
        };
    }

    // Get click analytics
    const clicks = await dynamoDB.query({
        TableName: 'click_analytics',
        IndexName: 'slug-timestamp-index',
        KeyConditionExpression: 'slug = :slug AND #ts BETWEEN :start AND :end',
        ExpressionAttributeNames: { '#ts': 'timestamp' },
        ExpressionAttributeValues: {
            ':slug': slug,
            ':start': getTimeRangeStart(timeRange),
            ':end': Date.now()
        }
    }).promise();

    // Aggregate data
    const analytics = {
        totalClicks: clicks.Items.length,
        uniqueClicks: new Set(clicks.Items.map(c => c.ip)).size,
        clicksByCountry: groupBy(clicks.Items, 'country'),
        clicksByHour: groupByHour(clicks.Items),
        topReferers: getTopReferers(clicks.Items),
        deviceTypes: analyzeUserAgents(clicks.Items)
    };

    return {
        statusCode: 200,
        body: JSON.stringify(analytics)
    };
};

Bulk Operations

exports.bulkCreate = async (event) => {
    const { urls, userId } = JSON.parse(event.body);

    if (urls.length > 100) {
        return {
            statusCode: 400,
            body: JSON.stringify({ error: 'Maximum 100 URLs per batch' })
        };
    }

    const results = [];
    const batchItems = [];

    for (const url of urls) {
        const slug = generateSlug();
        const item = {
            PutRequest: {
                Item: {
                    slug,
                    originalUrl: url,
                    userId,
                    createdAt: new Date().toISOString(),
                    clicks: 0,
                    isActive: true
                }
            }
        };

        batchItems.push(item);
        results.push({
            originalUrl: url,
            shortUrl: `https://snapit.li/${slug}`,
            slug
        });
    }

    // Batch write to DynamoDB
    await dynamoDB.batchWrite({
        RequestItems: {
            'short_urls': batchItems
        }
    }).promise();

    return {
        statusCode: 200,
        body: JSON.stringify({ results })
    };
};

React Frontend

function URLShortener() {
    const [url, setUrl] = useState('');
    const [customSlug, setCustomSlug] = useState('');
    const [result, setResult] = useState(null);

    const shortenUrl = async () => {
        const response = await fetch('/api/shorten', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ url, customSlug })
        });

        const data = await response.json();
        setResult(data);
    };

    const copyToClipboard = async (text) => {
        await navigator.clipboard.writeText(text);
        showToast('Copied to clipboard!');
    };

    return (
        <div className="url-shortener">
            <input
                type="url"
                value={url}
                onChange={(e) => setUrl(e.target.value)}
                placeholder="Enter URL to shorten"
                className="url-input"
            />

            <input
                type="text"
                value={customSlug}
                onChange={(e) => setCustomSlug(e.target.value)}
                placeholder="Custom slug (optional)"
                className="slug-input"
            />

            <button onClick={shortenUrl} className="shorten-btn">
                Shorten URL
            </button>

            {result && (
                <div className="result-card">
                    <p>Short URL:</p>
                    <div className="url-result">
                        <span>{result.shortUrl}</span>
                        <button onClick={() => copyToClipboard(result.shortUrl)}>
                            Copy
                        </button>
                    </div>
                </div>
            )}
        </div>
    );
}

DynamoDB Schema

short_urls:
- slug (string) - partition key
- originalUrl (string) - original URL
- userId (string) - GSI partition key
- createdAt (string) - creation timestamp
- clicks (number) - click count
- isActive (boolean) - active status
- expiresAt (string) - expiration date

click_analytics:
- id (string) - partition key
- slug (string) - GSI partition key
- timestamp (number) - GSI sort key
- ip (string) - source IP
- userAgent (string) - user agent
- referer (string) - referring site
- country (string) - geographic location

Custom Domain Setup

# CloudFormation template snippet
CustomDomain:
  Type: AWS::ApiGateway::DomainName
  Properties:
    DomainName: snapit.li
    CertificateArn: !Ref SSLCertificate
    SecurityPolicy: TLS_1_2

BasePathMapping:
  Type: AWS::ApiGateway::BasePathMapping
  Properties:
    DomainName: !Ref CustomDomain
    RestApiId: !Ref ApiGateway
    Stage: prod

Key Features

QR Code Integration

function generateQRCode(shortUrl) {
    return QRCode.toDataURL(shortUrl, {
        width: 256,
        margin: 2,
        color: {
            dark: '#000000',
            light: '#FFFFFF'
        }
    });
}

Link Expiration

// Scheduled Lambda to clean expired links
exports.cleanupExpired = async () => {
    const now = new Date().toISOString();

    const expired = await dynamoDB.scan({
        TableName: 'short_urls',
        FilterExpression: 'expiresAt < :now',
        ExpressionAttributeValues: { ':now': now }
    }).promise();

    for (const item of expired.Items) {
        await dynamoDB.update({
            TableName: 'short_urls',
            Key: { slug: item.slug },
            UpdateExpression: 'SET isActive = :false',
            ExpressionAttributeValues: { ':false': false }
        }).promise();
    }
};

Performance