Web Development, Privacy, JavaScript, TypeScript, Vue·

Building a Privacy-First URL Encoder: Why Client-Side Processing Matters

Learn how we built a lightweight URL encoding tool that respects your privacy by processing everything in your browser. Explore RFC 3986 standards, TextEncoder API, and performance optimization techniques.

Building a Privacy-First URL Encoder: Why Client-Side Processing Matters

In an era where data privacy is paramount, we built a URL encoding tool that processes everything directly in your browser—zero data leaves your device. This post explores the technical decisions behind our implementation and why privacy-first tooling matters.

Why URL Encoding Matters

URL encoding (percent-encoding) is essential for safely transmitting text over the internet. Characters like spaces, special symbols, and non-ASCII text (emoji, CJK characters) must be converted to a URL-safe format. Without proper encoding, URLs break, APIs reject requests, and data gets corrupted.

Common Use Cases:

  • API development: Encoding query parameters (?search=Hello World?search=Hello%20World)
  • QA testing: Generating test URLs with edge case characters
  • Marketing: Creating UTM parameters with special characters

Privacy Concerns with Online Tools

Most online URL encoders send your text to a server for processing. This raises several concerns:

  • Data logging: Your sensitive data (API keys, personal info) might be logged
  • Third-party tracking: Analytics scripts monitor what you encode
  • Network exposure: Data travels over the internet, increasing attack surface
  • Compliance issues: GDPR/HIPAA violations if sensitive data is processed server-side

Our Solution: All encoding happens in your browser using native JavaScript APIs. No servers, no tracking, no data transmission.

RFC 3986 Standards: Understanding Character Encoding

URL encoding follows RFC 3986 standards, which define three character classes:

1. Unreserved Characters (Never Encoded)

A-Z a-z 0-9 - . _ ~

These are always safe in URLs and remain unchanged in Basic Mode.

2. Reserved Characters (Encoded in Basic Mode)

: / ? # [ ] @ ! $ & ' ( ) * + , ; =

These have special meaning in URLs and must be encoded when used as data.

3. Unsafe Characters (Always Encoded)

space < > " { } | \ ^ `

These can break URL parsing and are always percent-encoded.

Example:

Input:  user@example.com?search=Hello World
Output: user%40example.com%3Fsearch%3DHello%20World

@ → %40 (reserved)
? → %3F (reserved)
space → %20 (unsafe)

TextEncoder API: Handling UTF-8 Multi-Byte Characters

JavaScript strings are UTF-16, but URLs use UTF-8 percent-encoding. This is critical for emoji and non-ASCII text.

The Challenge: Emoji Encoding

// ❌ WRONG: String.charCodeAt() fails for multi-byte characters
const emoji = '👋'
emoji.charCodeAt(0) // Returns 55357 (UTF-16 code unit, not UTF-8 bytes)

// ✅ CORRECT: TextEncoder converts to UTF-8 bytes
const encoder = new TextEncoder()
const bytes = encoder.encode('👋')
// Uint8Array([0xF0, 0x9F, 0x91, 0x8B])

const encoded = Array.from(bytes)
  .map(byte => '%' + byte.toString(16).toUpperCase().padStart(2, '0'))
  .join('')
// Result: "%F0%9F%91%8B"

Our Implementation

function encodeRFC3986(text: string, mode: 'basic' | 'advanced'): string {
  const encoder = new TextEncoder()

  return Array.from(text)
    .map(char => {
      // Basic mode: preserve unreserved characters
      if (mode === 'basic' && /[A-Za-z0-9\-._~]/.test(char)) {
        return char
      }

      // Encode to UTF-8 bytes then %XX format
      const bytes = encoder.encode(char)
      return Array.from(bytes)
        .map(byte => '%' + byte.toString(16).toUpperCase().padStart(2, '0'))
        .join('')
    })
    .join('')
}

Why TextEncoder?

  • ✅ Native browser API (Chrome 38+, Firefox 18+, Safari 10.1+)
  • ✅ Zero dependencies (no external libraries)
  • ✅ Correct UTF-8 encoding for all Unicode characters
  • ✅ Handles emoji, CJK characters, RTL text, and symbols

Performance Optimization: Debouncing and Large Inputs

Real-time encoding creates performance challenges. Here's how we optimized:

Debouncing Rapid Input Changes

const DEBOUNCE_MS = 150

let debounceTimer: ReturnType<typeof setTimeout> | null = null

function debounce(func: Function, delay: number) {
  return (...args: any[]) => {
    if (debounceTimer) clearTimeout(debounceTimer)
    debounceTimer = setTimeout(() => {
      func(...args)
      debounceTimer = null
    }, delay)
  }
}

// Apply debouncing to encoding
const debouncedEncode = debounce((text: string) => {
  encodedOutput.value = encodeRFC3986(text, encodingMode.value)
}, DEBOUNCE_MS)

watch(inputText, (newValue) => {
  debouncedEncode(newValue)
})

Why 150ms?

  • 100ms feels instant to users
  • Prevents encoding every single keystroke during rapid typing
  • Reduces CPU usage when pasting large text

Handling Large Inputs (10KB-1MB)

For bulk encoding, we show a progress indicator to prevent perceived freezing:

const WORKER_THRESHOLD = 100_000 // 100KB
const PROGRESS_DELAY = 500 // Only show spinner if >500ms

const debouncedEncode = debounce(async (text: string) => {
  let progressTimer: ReturnType<typeof setTimeout> | null = null

  if (text.length > WORKER_THRESHOLD) {
    // Wait 500ms before showing spinner (avoids flash for fast encoding)
    progressTimer = setTimeout(() => {
      isProcessing.value = true
    }, PROGRESS_DELAY)
  }

  try {
    encodedOutput.value = encodeRFC3986(text, encodingMode.value)
  } finally {
    if (progressTimer) clearTimeout(progressTimer)
    isProcessing.value = false
  }
}, DEBOUNCE_MS)

Performance Targets:

  • 50 characters: <100ms (instant)
  • 10KB: <1s (acceptable)
  • 100KB: <3s with progress indicator

Clipboard Security: Permissions and Fallback Patterns

Copying to clipboard requires careful permission handling.

Modern Clipboard API with Fallback

async function copyToClipboard(text: string): Promise<{ success: boolean; error?: string }> {
  try {
    // Modern Clipboard API (preferred)
    await navigator.clipboard.writeText(text)
    return { success: true }
  } catch (err) {
    // Fallback for older browsers or denied permissions
    try {
      const textarea = document.createElement('textarea')
      textarea.value = text
      textarea.style.position = 'fixed'
      textarea.style.opacity = '0'
      document.body.appendChild(textarea)
      textarea.select()
      const success = document.execCommand('copy')
      document.body.removeChild(textarea)

      if (!success) throw new Error('execCommand failed')
      return { success: true }
    } catch (fallbackErr) {
      return {
        success: false,
        error: 'Clipboard access denied. Please copy manually.'
      }
    }
  }
}

Why Two Approaches?

  • Clipboard API: Modern, secure, async (requires HTTPS)
  • execCommand: Deprecated but widely supported fallback
  • Graceful degradation: Users always get a clear error message if both fail

Security Considerations

  • HTTPS requirement: Clipboard API only works on secure contexts (localhost or HTTPS)
  • Permission prompts: Browsers may ask users to allow clipboard access
  • User notification: Show success/error messages with auto-dismiss

Lessons Learned: Client-Side Processing Trade-offs

Advantages

Privacy: Zero server transmission means zero data exposure ✅ Speed: No network latency—encoding is instant ✅ Offline: Works without internet connection ✅ Cost: No server infrastructure or bandwidth costs ✅ Simplicity: Fewer moving parts = fewer bugs

Limitations

⚠️ No persistence: State is lost on page refresh (intentional for privacy) ⚠️ Browser constraints: Limited to browser memory for very large inputs (>10MB) ⚠️ Feature detection: Must handle browsers lacking modern APIs

When to Use Server-Side Processing

Client-side isn't always appropriate:

  • Batch processing: Encoding thousands of URLs from a database
  • Integration: Tools that need to integrate with backend workflows
  • Analytics: When usage tracking is required for product insights

For simple, privacy-sensitive text transformations like URL encoding, client-side wins.

Next Steps

Try the tool at codecultivation.com/tools/url-encode

Features:

  • Basic Mode: RFC 3986 compliant encoding (recommended)
  • Advanced Mode: Encode all characters including alphanumerics
  • UTF-8 Support: Correctly handles emoji, CJK characters, and symbols
  • Clipboard Copy: One-click copy with fallback support
  • File Download: Save encoded text as .txt file
  • Privacy First: Zero data transmission, zero tracking

Conclusion

Building privacy-first tools requires thoughtful technical decisions. By leveraging browser-native APIs (TextEncoder, Clipboard, Blob), we created a fast, secure URL encoder without external dependencies or server infrastructure.

Key Takeaways:

  1. Use TextEncoder API for correct UTF-8 percent-encoding
  2. Implement debouncing for real-time encoding performance
  3. Provide clipboard fallbacks for maximum browser compatibility
  4. Prioritize privacy by processing client-side when possible
  5. Keep it simple—native APIs are often sufficient

The best tool is one that respects your data. No logging, no tracking, no servers—just pure encoding in your browser.

Code Cultivation • © 2026