DEV Community

Cover image for Building Dynamic QR Code Systems with Node.js
Daniel Saavedra
Daniel Saavedra

Posted on

Building Dynamic QR Code Systems with Node.js

What Are Dynamic QR Codes?

Traditional QR codes embed a static URL directly into their pattern. Once printed, that destination is permanent. Change your website? Your printed codes become useless.

Dynamic QR codes work differently. The printed pattern contains a short redirect URL (like uqr.ai/r/abc123). When scanned, this URL hits a server that looks up the current destination and redirects the user there. The destination can be updated anytime without changing the printed code.

How it works:

  1. You create a QR code with type dynamic (type_id: 2 in the API)
  2. The API generates a short URL that points to their redirect service
  3. You print this QR code anywhere
  4. Later, you can update where that short URL points to via API
  5. Scans always go to the current destination

This enables use cases like:

  • Restaurant menus that change daily
  • Product packaging that links to different campaigns by season
  • Event badges that show different info each day
  • Marketing materials that A/B test different landing pages

API Documentation Overview

The UQR.ai API provides a standard REST interface for managing dynamic QR codes.

Base URL

https://uqr.ai/api/v1
plain
Copy

Authentication

All requests require an API key in the header:
x-api-key: your_api_key_here
plain
Copy

Core Endpoints

Method Endpoint Description
POST /qr-codes Create a new QR code
GET /qr-codes List all QR codes with metadata
GET /qr-codes/:id Get specific QR code details
PUT /qr-codes/:id Update QR code (change destination, design, etc.)
DELETE /qr-codes/:id Remove a QR code

QR Code Types (type_id)

The API supports multiple QR code types:

  • 1 - URL: Standard link to a website
  • 2 - Dynamic URL: Changeable link (what we'll focus on)
  • 3 - WiFi: Network credentials encoded
  • 4 - Email: Pre-filled email composition
  • 5 - Phone: Dial a number
  • 6 - SMS: Send a text message
  • 7 - vCard: Contact information
  • 8 - Event: Calendar event
  • 9 - Geo: GPS coordinates
  • 10 - Bitcoin: Cryptocurrency payment

For dynamic functionality, we use type_id: 2.

Request/Response Format

Creating a QR code:


json
POST /qr-codes
{
  "name": "My Dynamic QR",
  "type_id": 2,
  "redirect_url": "https://example.com/landing",
  "options": {
    "dots_style": "rounded",
    "corner_style": "extra-rounded",
    "color": "#000000",
    "background_color": "#FFFFFF"
  }
}
Response:
JSON
Copy
{
  "id": "qr_abc123xyz",
  "name": "My Dynamic QR",
  "short_id": "abc123",
  "short_url": "https://uqr.ai/r/abc123",
  "redirect_url": "https://example.com/landing",
  "image": "https://cdn.uqr.ai/qr/abc123.png",
  "scan_count": 0,
  "created_at": "2024-03-09T12:00:00Z"
}
The short_url is what you print. The id is what you use to update it later.
Design Options
The API allows customizing the QR code appearance:
Dots Style (the pattern inside the QR):
square - Standard square blocks
dots - Circular dots
rounded - Rounded squares
extra-rounded - More rounded corners
classy - Elegant rounded style
classy-rounded - Extra elegant
Corner Style (the three position markers):
square - Standard corners
dot - Circular position markers
extra-rounded - Rounded position markers
Colors:
All colors accept hex codes:
color - The QR pattern color
background_color - The background (usually white)
corner_color - The position marker color
Premium Features
The API documentation mentions several features that require a paid subscription:
Custom short_id: Instead of random characters, use branded URLs like uqr.ai/r/my-brand
Password protection: Require a password before showing the destination
Expiration dates: Set a date when the QR code stops working
Scan limits: Limit total number of scans (useful for limited tickets)
Expiration redirect: Where to send users after expiration
Logo overlay: Add your company logo to the center of the QR
Webhook notifications: Get notified on every scan
These are controlled via fields like:
JSON
Copy
{
  "password_enabled": true,
  "link_password": "secret123",
  "expires_at": "2024-12-31T23:59:59Z",
  "scan_limit": 100,
  "expired_redirect_url": "https://example.com/expired"
}
Error Handling
The API returns standard HTTP status codes:
400 - Validation error (missing required fields, invalid data)
401 - Missing or invalid API key
403 - Feature requires premium subscription
404 - QR code not found (wrong ID)
429 - Rate limited (too many requests)
500 - Server error
Error responses include a JSON body:
JSON
Copy
{
  "error": "validation_error",
  "message": "Invalid redirect URL format"
}
Common error codes:
missing_api_key - No API key provided
invalid_api_key - Key doesn't exist
api_key_inactive - Key has been disabled
premium_required - Feature needs paid plan
validation_error - Request data is invalid
not_found - Resource doesn't exist
Implementation: Restaurant Menu System
Here's a complete implementation showing how dynamic QR codes solve real problems.
JavaScript
Copy
import { QRCodeClient } from './qr-client.js';

class RestaurantMenuSystem {
  #client;
  #tables = new Map();

  constructor(apiKey) {
    this.#client = new QRCodeClient(apiKey);
  }

  async setupTable(tableNumber, section = 'main') {
    const qr = await this.#client.create({
      name: `Table ${tableNumber}`,
      type_id: 2,
      options: {
        dots_style: 'rounded',
        color: section === 'vip' ? '#7c3aed' : '#2563eb'
      },
      short_id: `restaurant-t${tableNumber}`
    });

    this.#tables.set(tableNumber, {
      qrId: qr.id,
      shortUrl: qr.short_url,
      image: qr.image
    });

    return {
      tableNumber,
      qrCode: qr.short_url,
      qrImage: qr.image
    };
  }

  async switchMenu(menuType) {
    const urls = {
      lunch: 'https://restaurant.com/menu/lunch',
      dinner: 'https://restaurant.com/menu/dinner',
      happyhour: 'https://restaurant.com/happy-hour',
      brunch: 'https://restaurant.com/brunch'
    };

    const targetUrl = urls[menuType];
    if (!targetUrl) throw new Error(`Unknown menu type: ${menuType}`);

    const updates = [];
    for (const [tableNumber, data] of this.#tables) {
      await this.#client.update(data.qrId, { redirect_url: targetUrl });
      updates.push(tableNumber);
    }

    console.log(`Updated ${updates.length} tables to ${menuType} menu`);
    return updates;
  }

  async getAnalytics(tableNumber) {
    const table = this.#tables.get(tableNumber);
    if (!table) throw new Error('Table not found');

    const allCodes = await this.#client.list();
    const code = allCodes.find(c => c.id === table.qrId);

    return {
      table: tableNumber,
      scans: code?.scan_count ?? 0,
      unique: code?.unique_scan_count ?? 0,
      lastScan: code?.last_scan_at
    };
  }
}

// Usage
const restaurant = new RestaurantMenuSystem(process.env.QR_API_KEY);

// Setup tables
for (let i = 1; i <= 20; i++) {
  await restaurant.setupTable(i);
}

// Morning: lunch menu
await restaurant.switchMenu('lunch');

// 5 PM: switch all tables to dinner menu (same printed codes, new destination)
await restaurant.switchMenu('dinner');

// Check which tables are most active
const stats = await restaurant.getAnalytics(5);
console.log(`Table 5: ${stats.scans} scans`);
Client Implementation
Here's the base client that wraps the API:
JavaScript
Copy
const API_BASE = 'https://api.uqr.ai/v1';

export class QRCodeClient {
  #apiKey;

  constructor(apiKey) {
    this.#apiKey = apiKey;
  }

  #headers() {
    return {
      'x-api-key': this.#apiKey,
      'Content-Type': 'application/json'
    };
  }

  async create({ name, type_id = 2, options = {}, ...fields }) {
    const response = await fetch(`${API_BASE}/qr-codes`, {
      method: 'POST',
      headers: this.#headers(),
      body: JSON.stringify({
        name,
        type_id,
        options: {
          dots_style: options.dots_style ?? 'rounded',
          corner_style: options.corner_style ?? 'extra-rounded',
          color: options.color ?? '#000000',
          background_color: options.background_color ?? '#FFFFFF',
          ...options
        },
        ...fields
      })
    });

    if (!response.ok) {
      const error = await response.text();
      throw new Error(`HTTP ${response.status}: ${error}`);
    }

    return response.json();
  }

  async update(qrId, updates) {
    const response = await fetch(`${API_BASE}/qr-codes/${qrId}`, {
      method: 'PUT',
      headers: this.#headers(),
      body: JSON.stringify(updates)
    });

    if (!response.ok) throw new Error(`Update failed: ${response.status}`);
    return response.json();
  }

  async list() {
    const response = await fetch(`${API_BASE}/qr-codes`, {
      headers: this.#headers()
    });

    if (!response.ok) throw new Error(`List failed: ${response.status}`);
    const data = await response.json();
    return data.data ?? [];
  }
}
Rate Limiting and Retries
The API implements rate limiting. When you hit a 429 error, implement exponential backoff:
JavaScript
Copy
const sleep = (ms) => new Promise(r => setTimeout(r, ms));

async function withRetry(fn, { retries = 3, delay = 1000 } = {}) {
  for (let i = 0; i < retries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (error.message?.includes('429') && i < retries - 1) {
        await sleep(delay * Math.pow(2, i));
        continue;
      }
      throw error;
    }
  }
}

// Usage
const qr = await withRetry(
  () => client.create({ name: 'My QR' }),
  { retries: 5 }
);
Summary
Dynamic QR codes separate the printed pattern from the destination URL. This enables:
Updating links without reprinting materials
A/B testing different destinations
Time-based content (different menus by hour)
Analytics on scan behavior
Access control (passwords, expiration, scan limits)
The API provides standard REST endpoints for CRUD operations on QR codes, with extensive customization options for design and behavior. The key insight is that type_id: 2 (dynamic URL) creates codes that can be updated via the PUT endpoint while keeping the same printed image.
Enter fullscreen mode Exit fullscreen mode

Top comments (0)