A sleek, minimalist digital illustration depicting the URL shortening process. The image shows a long web address flowing into a funnel-like converter and emerging as a compact shortened URL. The design uses a professional blue and teal color scheme with white space and includes small analytics graphs representing click tracking functionality. This clean visualization perfectly represents the technical concept of URL shortening for a web development tutorial.

Building a URL Shortener Service: A Complete Guide (Updated May 2025)

Follow this step-by-step tutorial to build a complete URL shortener service using Node.js, Express, and MongoDB. Learn how to implement custom shortening algorithms, basic analytics, and a clean, responsive interface.

Building a URL Shortener Service: A Complete Guide (Updated May 2025)

SECURITY ALERT - MAY 14, 2025: Critical security vulnerabilities have been identified in Node.js, MongoDB, and Express.js that affect URL shortener implementations. These vulnerabilities are addressed in this updated guide. If you've implemented a URL shortener based on our previous instructions, please update your dependencies immediately.

Last Updated: May 12, 2025 Original Publication: April 2025

Security Updates

Several critical security vulnerabilities have been discovered in common URL shortener dependencies that require immediate attention:

Node.js Security Vulnerabilities

The Node.js team has announced critical security releases scheduled for May 14, 2025, affecting all versions from 20.x through 24.x with high severity issues. These vulnerabilities could potentially allow:

  • Remote code execution via malformed URL inputs

  • Denial of service attacks through resource exhaustion

  • Unauthorized access to protected routes

Required Action: Update to the patched versions as soon as they're available on May 14, 2025:

  • Node.js 20.x users should update to 20.15.0

  • Node.js 21.x users should update to 21.7.2

  • Node.js 22.x users should update to 22.3.1

  • Node.js 23.x users should update to 23.2.0

  • Node.js 24.x users should update to 24.0.1

In the meantime, implement the following temporary mitigation:

code
// Add the following validation to your URL input handling
function validateURL(url) {
  // Check for potentially malicious patterns
  const dangerousPatterns = [
    /\.\.\//g,           // Directory traversal
    /%00/g,              // Null byte injection
    /<script>/gi,        // Basic XSS attempt
    /javascript:/gi,     // JavaScript protocol
    /data:/gi            // Data URI 
  ];
  
  return !dangerousPatterns.some(pattern => pattern.test(url));
}

// Then use in your route handlers
app.post('/api/shorten', (req, res) => {
  const originalUrl = req.body.url;
  
  if (!validateURL(originalUrl)) {
    return res.status(400).json({ error: 'Invalid URL detected' });
  }
  
  // Continue with normal processing
  // ...
});

MongoDB Vulnerabilities

Multiple MongoDB vulnerabilities (CVE-2025-3085, CVE-2025-3084) impact versions prior to 5.0.31, 6.0.20, 7.0.16, and 8.0.4. These vulnerabilities could allow:

  • Unauthorized data access through improper access control

  • Data manipulation via injection attacks

  • Denial of service through resource exhaustion

Required Action: Update to the latest patched versions immediately:

# For npm users npm update mongodb # For MongoDB server instances # Follow instructions at https://www.mongodb.com/docs/manual/tutorial/upgrade-revision/

Additionally, implement proper input sanitization and parameterized queries:

code
// Instead of:
db.collection.find({ "url": userInput });

// Use:
db.collection.find({ "url": { $eq: userInput } });

Express.js Path-to-Regexp Vulnerability

Express.js version 4.21.1 and earlier contain a path-to-regexp vulnerability that could enable DoS attacks through specially crafted URLs. This is particularly dangerous for URL shortener services that handle a wide variety of input URLs.

Required Action: Update to Express.js 4.21.2 or later:

npm install express@latest

Comprehensive Security Checklist

✅ Update all dependencies to their latest secure versions ✅ Implement input validation for all user-provided URLs ✅ Use parameterized queries for all database operations ✅ Apply rate limiting to prevent abuse ✅ Implement proper error handling to prevent information disclosure ✅ Use HTTPS for all communications ✅ Enable Content Security Policy (CSP) headers

Introduction

URL shorteners have become an essential part of the modern web ecosystem, allowing users to share long, unwieldy URLs as concise, memorable links. They're widely used in social media, marketing campaigns, QR codes, and anywhere character count or readability matters.

This guide will walk you through building a secure, scalable, and feature-rich URL shortener service using Node.js, Express, and MongoDB. What sets this guide apart is our focus on both technical implementation and production-ready features, including:

  • Security-first development practices

  • Privacy protection for both link creators and users

  • Advanced analytics that respect user consent

  • Robust error handling and monitoring

  • Regulatory compliance (GDPR, CCPA, etc.)

By the end of this guide, you'll have a fully functional URL shortener that you can deploy for personal or commercial use.

Architecture Overview

Our URL shortener follows a three-tier architecture:

  1. Frontend

    • A responsive React application with Material-UI components

  2. Backend

    • Node.js with Express.js API framework

  3. Database

    • MongoDB for storing URL mappings and analytics

Key components include:

  • URL Processor

    • Validates, shortens, and stores URLs

  • Redirection Service

    • Handles link resolution and redirects

  • Analytics Engine

    • Tracks clicks and provides insights

  • Admin Dashboard

    • Manages links and views statistics

  • User Authentication

    • Optional user accounts for link management

The process flow is straightforward:

  1. User submits a long URL

  2. System validates the URL for security and format

  3. System generates a unique short code

  4. The code and original URL are stored in the database

  5. User receives the shortened URL

  6. When someone accesses the short URL, the system retrieves the original URL and redirects

Setting Up Your Development Environment

First, ensure you have the following installed:

  • Node.js (version 24.0.1 or later for security)

  • npm (9.0.0 or later)

  • MongoDB (version 8.0.4 or later for security)

  • Git

Create your project structure:

code
mkdir url-shortener
cd url-shortener
npm init -y

# Install secure dependencies

code
npm install express@4.21.2 mongoose@8.0.3 shortid@2.2.16 valid-url@1.0.9 
npm install cors helmet rate-limiter-flexible dotenv bcrypt jsonwebtoken
npm install --save-dev nodemon jest supertest

# Add React with security patches for frontend

code
npx create-react-app client
cd client
npm install @material-ui/core@latest axios@latest recharts@latest
cd ..

Create a .env file for environment variables:

code
NODE_ENV=development
PORT=5000
MONGO_URI=mongodb://localhost:27017/urlshortener
BASE_URL=http://localhost:5000
JWT_SECRET=your_jwt_secret_here
RATE_LIMIT_WINDOW=15
RATE_LIMIT_MAX=100

Note: For production, use proper secret management and never commit secrets to version control.

Backend Implementation

Server Setup

Create server.js with security headers and rate limiting:

code
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
const { RateLimiterMongo } = require('rate-limiter-flexible');
const dotenv = require('dotenv');

dotenv.config();

const app = express();

// Enhanced security headers
app.use(helmet());
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'unsafe-inline'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", 'data:'],
  }
}));

// Body parser middleware with size limits
app.use(express.json({ limit: '10kb' }));

// CORS with restricted origins
const corsOptions = {
  origin: process.env.NODE_ENV === 'production' 
    ? ['https://yourdomain.com'] 
    : ['http://localhost:3000'],
  optionsSuccessStatus: 200
};
app.use(cors(corsOptions));

// Connect to MongoDB with enhanced security
mongoose.connect(process.env.MONGO_URI, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  useCreateIndex: true,
  useFindAndModify: false,
  // Security improvements
  authSource: 'admin',
  retryWrites: true,
  w: 'majority'
})
.then(() => console.log('MongoDB Connected'))
.catch(err => {
  console.error('MongoDB Connection Error:', err);
  process.exit(1);
});

// Setup rate limiter to prevent abuse
const rateLimiterMongo = new RateLimiterMongo({
  storeClient: mongoose.connection,
  keyPrefix: 'middleware',
  points: process.env.RATE_LIMIT_MAX, 
  duration: process.env.RATE_LIMIT_WINDOW,
});

const rateLimiterMiddleware = (req, res, next) => {
  rateLimiterMongo.consume(req.ip)
    .then(() => {
      next();
    })
    .catch(() => {
      res.status(429).send('Too Many Requests');
    });
};

app.use(rateLimiterMiddleware);

// Define routes
app.use('/api/url', require('./routes/url'));
app.use('/api/users', require('./routes/users'));

// Serve static assets in production
if (process.env.NODE_ENV === 'production') {
  app.use(express.static('client/build'));
  
  app.get('*', (req, res) => {
    res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'));
  });
}

const PORT = process.env.PORT || 5000; app.listen(PORT, () => console.log(`Server running on port ${PORT}`));

URL Model

Create a models/Url.js file:

code
const mongoose = require('mongoose');

const UrlSchema = new mongoose.Schema({
  urlCode: {
    type: String,
    required: true,
    unique: true,
    trim: true
  },
  longUrl: {
    type: String,
    required: true,
    trim: true
  },
  shortUrl: {
    type: String,
    required: true
  },
  userId: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User',
    required: false
  },
  clicks: {
    type: Number,
    default: 0
  },
  createdAt: {
    type: Date,
    default: Date.now
  },
  expiresAt: {
    type: Date,
    default: function() {
      // Default expiration of 90 days
      return new Date(Date.now() + 90 * 24 * 60 * 60 * 1000);
    }
  },
  // Privacy-enhanced analytics storage
  analytics: {
    referrers: [{
      source: String,
      count: Number
    }],
    browsers: [{
      name: String,
      count: Number
    }],
    devices: [{
      type: String,
      count: Number
    }],
    locations: [{
      country: String, 
      region: String,
      count: Number
    }],
    timeAccessed: [{
      hour: Number,
      count: Number
    }]
  },
  // Privacy settings
  privacySettings: {
    collectAnalytics: {
      type: Boolean,
      default: false
    },
    ipAnonymization: {
      type: Boolean,
      default: true
    },
    passwordProtected: {
      type: Boolean,
      default: false
    },
    password: {
      type: String,
      select: false
    }
  }
});

// Add indexes for better performance
UrlSchema.index({ urlCode: 1 });
UrlSchema.index({ userId: 1 });
UrlSchema.index({ createdAt: 1 });
UrlSchema.index({ expiresAt: 1 });

module.exports = mongoose.model('Url', UrlSchema);

URL Shortening Route

Create a routes/url.js file:

code
const express = require('express');
const router = express.Router();
const validUrl = require('valid-url');
const shortid = require('shortid');
const Url = require('../models/Url');
const auth = require('../middleware/auth');
const bcrypt = require('bcrypt');

// Input validation functions
function validateURL(url) {
  // Security check for potentially malicious patterns
  const dangerousPatterns = [
    /\.\.\//g,           // Directory traversal
    /%00/g,              // Null byte injection
    /<script>/gi,        // Basic XSS attempt
    /javascript:/gi,     // JavaScript protocol
    /data:/gi            // Data URI
  ];
  
  // Check if URL is valid and doesn't contain dangerous patterns
  return validUrl.isUri(url) && !dangerousPatterns.some(pattern => pattern.test(url));
}

// Shorten URL route
router.post('/shorten', async (req, res) => {
  try {
    const { longUrl, customCode, expiresIn, privacySettings } = req.body;
    const baseUrl = process.env.BASE_URL;
    
    // Check base url
    if (!validUrl.isUri(baseUrl)) {
      return res.status(400).json({ error: 'Invalid base URL' });
    }

    // Validate the long URL
    if (!validateURL(longUrl)) {
      return res.status(400).json({ error: 'Invalid URL format or potential security risk detected' });
    }

    // Create URL code - either custom or generated
    let urlCode = customCode || shortid.generate();
    
    // Check if custom code already exists
    if (customCode) {
      const existingUrl = await Url.findOne({ urlCode });
      if (existingUrl) {
        return res.status(400).json({ error: 'Custom code already in use' });
      }
    }

    // Calculate expiration date if provided
    let expiresAt = null;
    if (expiresIn) {
      expiresAt = new Date(Date.now() + parseInt(expiresIn) * 24 * 60 * 60 * 1000);
    }

    // Process privacy settings
    let securitySettings = {
      collectAnalytics: false,
      ipAnonymization: true,
      passwordProtected: false
    };
    
    if (privacySettings) {
      securitySettings = {
        ...securitySettings,
        ...privacySettings
      };
    }
    
    // Hash password if provided
    if (securitySettings.passwordProtected && req.body.password) {
      const salt = await bcrypt.genSalt(10);
      securitySettings.password = await bcrypt.hash(req.body.password, salt);
    }

    // Create short URL
    const shortUrl = `${baseUrl}/${urlCode}`;

    // Create URL object
    const url = new Url({
      longUrl,
      shortUrl,
      urlCode,
      createdAt: new Date(),
      expiresAt: expiresAt || undefined,
      userId: req.user ? req.user.id : null,
      privacySettings: securitySettings
    });

    // Save to database with error handling
    try {
      await url.save();
      res.status(201).json(url);
    } catch (err) {
      console.error('Database save error:', err);
      res.status(500).json({ error: 'Server error during URL creation' });
    }
  } catch (err) {
    console.error('URL shortening error:', err);
    res.status(500).json({ error: 'Server error' });
  }
});

// Redirect route
router.get('/:code', async (req, res) => {
  try {
    const { code } = req.params;
    
    // Fetch URL document
    const url = await Url.findOne({ 
      urlCode: code,
      expiresAt: { $gt: Date.now() } // Check it hasn't expired
    });

    if (!url) {
      return res.status(404).json({ error: 'URL not found or has expired' });
    }

    // Check if password protected
    if (url.privacySettings.passwordProtected) {
      // This route would just check if protection exists
      // Actual password verification would happen in a separate route
      return res.status(403).json({ 
        requiresPassword: true,
        urlCode: code
      });
    }

    // Update click count
    url.clicks++;
    
    // Update analytics if enabled
    if (url.privacySettings.collectAnalytics) {
      const referrer = req.get('Referrer') || 'Direct';
      const userAgent = req.get('User-Agent');
      
      // Update referrer analytics
      const referrerIndex = url.analytics.referrers.findIndex(r => r.source === referrer);
      if (referrerIndex > -1) {
        url.analytics.referrers[referrerIndex].count++;
      } else {
        url.analytics.referrers.push({ source: referrer, count: 1 });
      }
      
      // Basic browser detection
      let browser = 'Unknown';
      if (userAgent.includes('Chrome')) browser = 'Chrome';
      else if (userAgent.includes('Firefox')) browser = 'Firefox';
      else if (userAgent.includes('Safari')) browser = 'Safari';
      else if (userAgent.includes('Edge')) browser = 'Edge';
      
      const browserIndex = url.analytics.browsers.findIndex(b => b.name === browser);
      if (browserIndex > -1) {
        url.analytics.browsers[browserIndex].count++;
      } else {
        url.analytics.browsers.push({ name: browser, count: 1 });
      }
      
      // Basic device detection
      let device = 'Desktop';
      if (userAgent.includes('Mobile')) device = 'Mobile';
      else if (userAgent.includes('Tablet')) device = 'Tablet';
      
      const deviceIndex = url.analytics.devices.findIndex(d => d.type === device);
      if (deviceIndex > -1) {
        url.analytics.devices[deviceIndex].count++;
      } else {
        url.analytics.devices.push({ type: device, count: 1 });
      }
      
      // Time accessed analytics
      const hour = new Date().getHours();
      const timeIndex = url.analytics.timeAccessed.findIndex(t => t.hour === hour);
      if (timeIndex > -1) {
        url.analytics.timeAccessed[timeIndex].count++;
      } else {
        url.analytics.timeAccessed.push({ hour, count: 1 });
      }
    }
    
    // Save changes
    await url.save();
    
    // Redirect to the long URL
    return res.redirect(url.longUrl);
  } catch (err) {
    console.error('URL redirect error:', err);
    res.status(500).json({ error: 'Server error during redirect' });
  }
});

// Verify password for protected URLs
router.post('/:code/verify', async (req, res) => {
  try {
    const { code } = req.params;
    const { password } = req.body;
    
    const url = await Url.findOne({ urlCode: code }).select('+privacySettings.password');
    
    if (!url) {
      return res.status(404).json({ error: 'URL not found' });
    }
    
    if (!url.privacySettings.passwordProtected) {
      return res.status(400).json({ error: 'URL is not password protected' });
    }
    
    const isMatch = await bcrypt.compare(password, url.privacySettings.password);
    
    if (!isMatch) {
      return res.status(401).json({ error: 'Invalid password' });
    }
    
    // Password verified, redirect to the long URL
    return res.json({ success: true, longUrl: url.longUrl });
  } catch (err) {
    console.error('Password verification error:', err);
    res.status(500).json({ error: 'Server error during password verification' });
  }
});

// Additional routes omitted for brevity...

module.exports = router;

Database Design

Our MongoDB schema is designed with the following considerations:

Indexes for Performance

We've created indexes on the most frequently queried fields:

  • urlCode

    • For fast redirects

  • userId

    • For retrieving a user's URLs

  • createdAt

    and

    expiresAt

    • For maintenance and cleanup

Data Security Measures

  • Passwords are hashed using bcrypt

  • Analytics data is aggregated to avoid storing PII

  • IP addresses are anonymized by default

  • Expired URLs are automatically cleaned up by a scheduled job

Database Maintenance

Create a scheduled job to clean up expired URLs:

code
// scheduledJobs.js
const cron = require('node-cron');
const Url = require('./models/Url');

// Run daily at midnight
cron.schedule('0 0 * * *', async () => {
  try {
    console.log('Running URL cleanup job');
    
    // Delete expired URLs
    const result = await Url.deleteMany({
      expiresAt: { $lt: new Date() }
    });
    
    console.log(`Deleted ${result.deletedCount} expired URLs`);
  } catch (err) {
    console.error('URL cleanup job error:', err);
  }
});

Privacy-First Implementation

In the past year, privacy concerns have become even more critical for URL shortener services. Our updated implementation now follows these privacy-first principles:

Consent-Based Analytics Collection

code
// Frontend component (React)
function PrivacyConsentForm({ onChange }) {
  return (
    <FormControl component="fieldset" margin="normal">
      <FormLabel component="legend">Privacy Settings</FormLabel>
      <FormGroup>
        <FormControlLabel
          control={<Switch onChange={(e) => onChange('collectAnalytics', e.target.checked)} />}
          label="Allow anonymous analytics (helps us improve the service)"
        />
        <FormControlLabel
          control={<Switch defaultChecked onChange={(e) => onChange('ipAnonymization', e.target.checked)} />}
          label="Enable IP anonymization"
        />
        <FormControlLabel
          control={<Switch onChange={(e) => onChange('passwordProtected', e.target.checked)} />}
          label="Password protect this link"
        />
      </FormGroup>
      <FormHelperText>
        We respect your privacy and that of your link visitors.
        No personally identifiable information is stored.
      </FormHelperText>
    </FormControl>
  );
}

IP Anonymization Implementation

code
// Function to anonymize IP addresses
function anonymizeIP(ip) {
  if (!ip) return '';
  
  // For IPv4, remove the last octet
  if (ip.includes('.')) {
    return ip.replace(/\.\d+$/, '.0');
  }
  
  // For IPv6, remove the last 80 bits (last 5 segments)
  if (ip.includes(':')) {
    return ip.replace(/:[^:]+:[^:]+:[^:]+:[^:]+:[^:]+$/, ':0:0:0:0:0');
  }
  
  return ip;
}

// Usage in route handler
if (url.privacySettings.collectAnalytics) {
  let visitorIP = req.ip;
  
  if (url.privacySettings.ipAnonymization) {
    visitorIP = anonymizeIP(visitorIP);
  }
  
  // Now use the anonymized IP for geolocation or analytics
  // ...
}

GDPR-Compliant Data Handling

Our URL shortener now follows these GDPR principles:

  • Data Minimization

    • We only collect what's necessary

  • Purpose Limitation

    • Data is only used for its stated purpose

  • Storage Limitation

    • URLs expire by default after 90 days

  • Right to Be Forgotten

    • Users can delete their links at any time

  • Transparency

    • Clear privacy settings and information

Advanced Analytics

Based on recent innovations in URL analytics, our system now incorporates Dub's Conversion Analytics approach, which tracks the entire user journey:

code
// Enhanced analytics schema in models/Url.js
analytics: {
  // Basic analytics from before
  referrers: [{ source: String, count: Number }],
  browsers: [{ name: String, count: Number }],
  devices: [{ type: String, count: Number }],
  
  // New conversion tracking
  conversions: [{
    goalType: {
      type: String,
      enum: ['pageView', 'buttonClick', 'formSubmission', 'purchase']
    },
    count: Number,
    value: Number // For tracking monetary value
  }],
  
  // Time-on-site tracking (anonymized)
  engagementMetrics: [{
    timeOnSite: Number, // seconds
    pageDepth: Number,  // number of pages visited
    count: Number       // occurrences of this pattern
  }],
  
  // Multi-device journey tracking (anonymized)
  crossDeviceJourneys: [{
    deviceSequence: [String], // e.g., ['Mobile', 'Desktop']
    count: Number
  }]
}

QR Code Integration

One of the most requested features has been tighter QR code integration. Our updated implementation includes dynamic QR codes that can be updated without changing printed materials:

code
// routes/qrcode.js
const express = require('express');
const router = express.Router();
const QRCode = require('qrcode');
const Url = require('../models/Url');

// Generate QR code for a short URL
router.get('/:code', async (req, res) => {
  try {
    const { code } = req.params;
    
    // Fetch URL
    const url = await Url.findOne({ urlCode: code });
    
    if (!url) {
      return res.status(404).json({ error: 'URL not found' });
    }
    
    // Get customization options
    const { size = 300, color = '000', logo = false } = req.query;
    
    // QR code options
    const options = {
      errorCorrectionLevel: 'H',
      type: 'image/png',
      quality: 0.92,
      margin: 1,
      color: {
        dark: `#${color}`,
        light: '#FFFFFF'
      },
      width: parseInt(size)
    };
    
    // Generate QR code as data URL
    const qrDataURL = await QRCode.toDataURL(url.shortUrl, options);
    
    // Convert data URL to buffer
    const qrBuffer = Buffer.from(qrDataURL.split(',')[1], 'base64');
    
    // Set content type and send
    res.setHeader('Content-Type', 'image/png');
    res.setHeader('Content-Disposition', `inline; filename="qr-${code}.png"`);
    res.send(qrBuffer);
  } catch (err) {
    console.error('QR code generation error:', err);
    res.status(500).json({ error: 'Server error during QR code generation' });
  }
});

// Create branded QR code with tracking
router.post('/branded', async (req, res) => {
  try {
    const { urlCode, brandColor, logo, trackingParams } = req.body;
    
    // Fetch URL
    const url = await Url.findOne({ urlCode });
    
    if (!url) {
      return res.status(404).json({ error: 'URL not found' });
    }
    
    // Add tracking parameters if requested
    let trackingUrl = url.shortUrl;
    if (trackingParams) {
      const separator = url.shortUrl.includes('?') ? '&' : '?';
      trackingUrl = `${url.shortUrl}${separator}${new URLSearchParams(trackingParams)}`;
    }
    
    // Create custom branded QR code
    // Implementation details omitted for brevity
    
    res.status(200).json({ success: true, qrCodeUrl: `/api/qrcode/${urlCode}/download` });
  } catch (err) {
    console.error('Branded QR code error:', err);
    res.status(500).json({ error: 'Server error during branded QR code creation' });
  }
});

module.exports = router;

Legal Compliance

Regulatory Requirements

URL shorteners must comply with various regulations, including:

  1. GDPR (EU)

    • User consent for analytics

    • Data minimization

    • Right to erasure

  2. CCPA/CPRA (California)

    • Right to know what data is collected

    • Right to delete personal information

    • Right to opt-out of data selling

  3. LGPD (Brazil)

    • Similar to GDPR but with Brazilian specifics

    • Requires consent for data processing

  4. State Privacy Laws in the US

    • New comprehensive state privacy laws in Connecticut, Colorado, Virginia, and Utah have expanded privacy requirements

    • These laws affect URL shorteners that collect any visitor data

Implementing Legal Compliance

  1. Privacy Policy Route

code
router.get('/privacy-policy', (req, res) => {
  res.render('privacy', {
    lastUpdated: 'May 12, 2025',
    dataCollected: [
      'URLs shortened (required for service functionality)',
      'Click statistics (with consent)',
      'Anonymized geographic data (with consent)',
      'Browser and device information (with consent)'
    ],
    dataPurposes: [
      'Providing URL redirection service',
      'Providing analytics to link creators (with consent)',
      'Improving our service'
    ],
    retentionPeriod: '90 days by default, or until manually deleted',
    userRights: [
      'Right to access your data',
      'Right to delete your data',
      'Right to opt out of analytics'
    ]
  });
});
  1. Data Access and Deletion Routes

/

code
/ Data access request
router.get('/api/user/data', auth, async (req, res) => {
  try {
    const userData = await User.findById(req.user.id).select('-password');
    const userUrls = await Url.find({ userId: req.user.id });
    
    res.json({
      userData,
      urls: userUrls.map(url => ({
        longUrl: url.longUrl,
        shortUrl: url.shortUrl,
        createdAt: url.createdAt,
        expiresAt: url.expiresAt,
        clicks: url.clicks,
        privacySettings: url.privacySettings
      }))
    });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Server error' });
  }
});

// Data deletion request
router.delete('/api/user/data', auth, async (req, res) => {
  try {
    // Delete all user's URLs
    await Url.deleteMany({ userId: req.user.id });
    
    // Delete user account if requested
    if (req.query.deleteAccount === 'true') {
      await User.findByIdAndDelete(req.user.id);
    }
    
    res.json({ message: 'Data deleted successfully' });
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Server error' });
  }
});

Conclusion

This updated guide has walked you through building a modern, secure URL shortener service with a focus on:

  1. Critical security updates

    to protect against newly discovered vulnerabilities

  2. Privacy-first implementation

    with consent-based data collection

  3. Advanced analytics capabilities

    for tracking the complete user journey

  4. QR code integration

    with dynamic and branded options

  5. Legal compliance

    with the latest privacy regulations

By following these best practices, you'll create a URL shortener that not only functions well but also respects user privacy and maintains the highest security standards.

Remember to stay updated on security patches and regularly review your implementation as new vulnerabilities and privacy requirements emerge.

Additional Resources


Last updated: May 12, 2025. This guide incorporates the latest security patches and privacy practices as of that date.

CrashBytes

Empowering technology professionals with actionable insights into emerging trends and practical solutions in software engineering, DevOps, and cloud architecture.

HomeBlogImagesAboutContactSitemap

© 2025 CrashBytes. All rights reserved. Built with ⚡ and Next.js